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

【Rails】flashメッセージをフェードアウトで消す方法【JavaScript】

はじめに

この記事では、flashメッセージを表示したあと一定時間後にフェードアウトさせる方法を解説します。

flashメッセージ系の記事はたくさんあるのですが、
どれもbootstrapを使用してたり
何やら複雑な方法だったり(Hamlハムル??とかrenderとかややこしい!)。。

もっと簡単な記事はないのか!とモヤモヤしたので、自分まとめます。
自分と同じような人の為に!!(自分用のメモですすみませんw)

flashメッセージの表示方法

それではレッツ実装!!

まずはコントローラーから。

コントローラー

def destroy
   @review = Review.find(params[:id])
   @review.destroy
   if review.destroy
      redirect_to root_path, notice: "︎レビューを削除しました!"
   end
end

メッセージを表示させる記述は4行目のif文からです。

レビューが削除された後、root_path(ホーム画面)に戻って
「レビューを削除しました」というメッセージが表示される。という流れです。

続いてビュー画面へ!

ビュー

<% if flash[:notice] %>
   <div class="flash"><%= flash[:notice] %></div>
<% end %>

<%= yield %>

flashメッセージはいろんな場面で共通で使う事が多いので、
views/layouts/application.html.erb
<%= yield %>より上の部分に記述します。

コントローラーとビューはひとまず完成!!

JavaScriptの下準備

まずはGemfile以下のコードを記述します。

gem 'jquery-rails'

からのターミナルで$ bundle install

続いて
app/assets/javascripts/application.js
に以下のコードを加えましょう。

//= require jquery

これでjQueryの下準備完了。

flashメッセージをフェードアウトさせる方法

いよいよ実装していきます。

①jsファイルに記述する場合(推奨)

以下のコードを
app/assets/javascripts/application.js(上と同じ場所)
に加えたら完成!!

$(function(){
  setTimeout("$('.flash').fadeOut('slow')", 2000);
});

②ビューファイルに直接記述する場合

以下のコードを
flashメッセージを表示させたいビューファイルに加えたら完成!!

<script>
  $(function() {
    setTimeout("$('.flash').fadeOut('slow')", 2000);
  });
</script>

※「.flash」はapplicaton.html.erbのdivに付けたクラス名です。各自自由に命名しましょう。
※数字部分は好みに合わせて変えましょう!ちなみに1000で1秒です。

さいごに

今回はレビューの削除destroy後の実装でしたが、もちろん編集edit
お気に入りfavoriteなんかも実装可能です。

あとはお好みでCSSをいじれば、
よく見るflashメッセージの完成です!!

<参考>
https://qiita.com/dir_sh0606/items/b2165459deda97ae8468

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

WEBページに「Googleアカウントでログイン」を実装する

目次

  • はじめに
  • 目標
  • クライアントIDを取得する
  • クライアントサイドのコード(JavaScript)
  • サーバサイドのコード(PHP)
  • 実行してみる
  • お世話になったサイト

はじめに

WEBアプリを作っていて、「Googleアカウントでログイン」が実装できたので備忘録として残しておく。
実装にあたっては、Googleの公式ドキュメントを大いに参考にしました。動画も一緒に載っていて、日本人が解説しています。Japanese Englishですごく聞き取りやすくて助かりました。皮肉じゃないです。日本語の方がうれしいんですけどね
PHPとJSを使って実装しています。サーバサイドの言語は自分はPHPを使いましたが、公式ドキュメントにはNode.jsとJavaとPythonのコードも載ってました。自分はPHPしか試していません。

必要なもの

  • テキストエディタ
  • サーバ(ローカル環境でも可、試してないから知らんけど)
  • ブラウザ
  • Composer(PHPを使ったので必要になった)
  • PHP(Composerを使うので)

目標

GoogleアカウントでログインされたユーザのGoogleアカウントのプロフィール情報を取得し、ログイン状態を付与する、というプログラムを書きます。作成するファイルは、以下の通りです。

  • Googleアカウントでログインするためのページを表示させるファイル(HTML)
  • ユーザのプロフィール情報を受け取るための処理をするファイル(PHP)
  • ログイン成功後に遷移するページさせるファイル(PHP)

表示するページのファイルは2つ、バックエンドの処理を行うファイルが1つ、計3つのファイルを作成します。ここでは最低限しか作っていないので、ユーザのプロフィールを取得していろいろとやりたいことがある場合はこのほかにいろいろ処理を足すことになります。

クライアントIDを取得する

API使用の際に定番の、クライアントID取得がまずは必要です。GCPから、プロジェクトの作成を行います。
プロジェクトのホームから、APIとサービス>認証情報 を選択します。
image.png

画面左上の「認証情報を作成」を選択します。
image.png

作成するのは「OAuthクライアントID」です。
image.png

アプリケーションの種類を「ウェブアプリケーション」と選択し、入力事項を埋めます。
image.png

作成が完了するとクライアントIDが発行されます。あとで必要になります。

クライアントサイドのコード

ログインさせるページ

HTMLとJSのみで実装します。クライアントIDを認識させ、Google Platform Libraryを読み込みます。HTMLファイルのheadタグ内に以下のコードをコピペします。

<script src="https://apis.google.com/js/platform.js" async defer></script>
<meta name="google-signin-client_id" content="<用意したクライアントID>">

bodyタグ内にはGoogleアカウントでログインボタンを設置します。これもGoogleが用意してくれています。

<div class="g-signin2" data-onsuccess="onSignIn"></div>

getBasicProfile()メソッドを使うことで、ログインしたユーザのプロフィール情報を取得できます。scriptタグ内に以下
を記述します。

function onSignIn(googleUser) {
  var profile = googleUser.getBasicProfile();
  console.log('ID: ' + profile.getId()); // Do not send to your backend! Use an ID token instead.
  console.log('Name: ' + profile.getName());
  console.log('Image URL: ' + profile.getImageUrl());
  console.log('Email: ' + profile.getEmail()); // This is null if the 'email' scope is not present.
}

コンソールにユーザのプロフィールが表示されているかと思います。これでユーザの識別がクライアント側でできたことになります。ここで取得しているプロフィール情報はクライアント側でしか持っていません。このGoogleアカウントのプロフィール情報を自サイトにログインするのに使うために、サーバに送信する必要があります。しかし、ここで注意しなければならないことがあります。

  • gmailのアドレスを個人の識別に使ってはいけない。代わりにGoogle IDまたはsub(後述)を使う。
  • なりすましのリスクがあるため、getId()で得られたGoogle IDを生でサーバに送信してはいけない。代わりにIDトークンを送信する。

ユーザの識別にはIDを使え、だがIDは直接送らず、IDトークンを送れ、ってことです。IDトークンはただの文字列です。サーバ側でそのトークンをもとにユーザのプロフィール情報を取得することが可能です。

ってなわけでIDトークンをサーバにPOST送信します。
まずIDトークンを取得します。さっきコピペした関数の中身を変えます。POST送信ができれば別にこの書き方にこだわる必要はありません。個人的にjQueryのajax()のほうがわかりやすくて好きなので自分はそっちで書きましたがうまくいきました。

function onSignIn(googleUser) {
  var id_token = googleUser.getAuthResponse().id_token;
  var xhr = new XMLHttpRequest();
  xhr.open('POST', 'https://yourbackend.example.com/tokensignin');
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  xhr.onload = function() {
    console.log('Signed in as: ' + xhr.responseText);
  };
  xhr.send('idtoken=' + id_token);
}

サーバサイドのコード

POST送信されたIDトークンを受け取り、トークンを照合します。トークンの称号にはGoogle API Client Libraryが必要です。Composerを使ってインストールします。Composerの使い方までは解説しません。自分もよく知らないからです。

composer require google/apiclient

POSTでIDトークンを受け取り、照合しています。

<?php 
require_once 'vendor/autoload.php';

$id_token = filter_input(INPUT_POST, 'id_token');
define('CLIENT_ID', '691530918857-qpu8h3bmt1fmd3onouvlu7dgcu46tlmn.apps.googleusercontent.com');

$client = new Google_Client(['client_id' => CLIENT_ID]); 
$payload = $client->verifyIdToken($id_token);
if ($payload) {
  $userid = $payload['sub'];
}

照合に成功したら、$payloadにプロフィール情報が入ります。$payloadの中身は以下のようになってます。

{
 // These six fields are included in all Google ID Tokens.
 "iss": "https://accounts.google.com",
 "sub": "110169484474386276334",
 "azp": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
 "aud": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
 "iat": "1433978353",
 "exp": "1433981953",

 // These seven fields are only included when the user has granted the "profile" and
 // "email" OAuth scopes to the application.
 "email": "testuser@gmail.com",
 "email_verified": "true",
 "name" : "Test User",
 "picture": "https://lh4.googleusercontent.com/-kYgzyAWpZzJ/ABCDEFGHI/AAAJKLMNOP/tIXL9Ir44LE/s99-c/photo.jpg",
 "given_name": "Test",
 "family_name": "User",
 "locale": "en"
}

ユーザの識別をするためのIDは$payload['sub']の値です。gmailのユーザ名じゃないんや~?

このIDを使ってユーザの識別ができたらセッション変数でも使ってログイン状態を付与し、ログイン後のページに遷移します。

実行してみる

login.html
<html>
<head>
    <script src="https://apis.google.com/js/platform.js" async defer></script>
    <meta name="google-signin-client_id" content="<用意したクライアントID>">
</head>
<body>
    <div class="g-signin2" data-onsuccess="onSignIn"></div>
    <script>
        function onSignIn(googleUser) {
            var id_token = googleUser.getAuthResponse().id_token;
            var xhr = new XMLHttpRequest();
            xhr.open('POST', 'test.php');
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
            xhr.onload = function() {
                console.log('Signed in as: ' + xhr.responseText);
            };
            xhr.send('idtoken=' + id_token);
            window.location.href = 'index.php';
        }
    </script>
</body>
</html>

test.php
<?php
session_start();
require_once 'vendor/autoload.php';

$id_token = filter_input(INPUT_POST, 'id_token');
define('CLIENT_ID', '<用意したクライアントID>');

$client = new Google_Client(['client_id' => CLIENT_ID]); 
$payload = $client->verifyIdToken($id_token);
if ($payload) {
    $userid = $payload['sub'];
}

//DBとのやりとりする

$_SESSION['login'] = true;
exit;
index.php
<?php
session_start();
if(!$_SESSION['login']){
    header('Location: login.html');
}
?>
<html>
<body>
    ログイン成功!    
</body>
</html>

login.phpにポツンと置かれたログインボタンからGoogleアカウントを選択してログインすれば、いつのまにかinndex.phpに飛んでいてくれるはず。

お世話になったサイト

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

制約の多い運用現場でエンジニアっぽいことをする方法

対象

  • セキュリティの関係でソフトやツールをインストール・導入できない
  • お金がないからツールを購入してもらえない
  • サクラエディタでコードを書いてる
  • そもそも周りにコードかいてるやつがいない
  • IEが推奨ブラウザ(そもそも外部にアクセスできない)
  • 何でもかんでもExcelでやりたがるetc.

上記はほとんど自分が今の現場に感じている不満だがおそらく自分だけじゃないはず.今や自宅での環境のほうが優れている人がほとんどだと思う.WEB系で働いている人のツイートとかみるとめっちゃ羨ましく思う.でもWindowsにはメモ帳があるじゃないか.

PowerShell

用途としては単純作業の自動化がメイン

  • メリット

    • 個人的に最近ハマっている
    • Windowsに標準でインストールされている
    • PowerShell ISEという統合開発環境がある
    • .NET Frameworkを利用できる
    • COMオブジェクトの操作が可能
    • CSV,XML,JSONを扱いやすい(これで少しは脱Excelできるかも)
    • レスポンスがLinuxのシェルと違って文字列ではなく,オブジェクトで返ってくるので扱いやすい
  • デメリット

    • ユーザが少ないので情報も少ない
    • 実行ポリシーが存在していてバッチファイルみたいにダブルクリックしてすぐ実行とはならない

HTML/CSS/JavaScript/VBScript

静的なポータルサイトとかアプリケーション

  • HTA(html Applications)でアプリ化できる
    • IEがあれば実行できる
    • AxtiveXが使える
  • ファイルサーバで静的サイトみたいに運用できる

C#

PowerShellの上位互換という感じでアプリとかちょっとしたスクリプトも可能

  • .NET Frameworkを利用できる
  • コマンドプロンプトでコンパイルできる(VisualStudioなくても開発可)
  • 細かいとこまろで作りこむことができる

まとめ

世の中にはもっと便利なものがたくさんあるが,基本的にWindowsで標準で入ってるものをうまく利用していくしかない.あのライブラリ使えたら楽だなーとかおもうこともあるが,利用できる環境じゃなければ自分で作っていくしかない.車輪の再発明かもしれないけど,この経験は無駄にはならないと思うので同じような境遇の方には是非試してもらいたい.またこの他に使えるものがあればぜひ教えてくださいー

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

Vue.js開発の達人への道

Vue開発を初めて使用する場合、シングルページアプリ、動的 & 非同期コンポーネント、サーバーサイドレンダリングなど、多くの専門用語が使用されていることを聞いたことがあるでしょう。

また、Vuex、Webpack、Vue CLI、Nuxtなど、Vueと一緒によく言及されるツールやライブラリについて聞いたことがあるかもしれません。

おそらく、この無数の用語とツールはフラストレーションになると思うでしょう。その中であなたは一人ではありません。すべての経験レベルの開発者は、自分が知らないことや、そうすべきだと感じているという永続的なプレッシャーを感じています。

0. JavaScriptと基本的なWeb開発

中国語で書かれた本のすべてを学ぶように頼んだら、まず中国語を読むことを学ぶ必要がありますよね?

同様に、VueはWebユーザーインターフェイスを構築するためのJavaScriptフレームワークです。 Vueを使用する前に、JavaScriptとWeb開発の基本を理解する必要があります。

FreeCodeCampでJavaScriptと基本的なWeb開発から上級な開発まで無料で学べます
https://www.freecodecamp.com/

1. Vueの基本的な概念

新しいVue開発者であれば、Vueコアライブラリ、Vueルーター、Vuexを含むVue.jsエコシステムの中核に注目する必要があります。

これらのツールは、ほとんどのVueアプリで機能し、このマップの他のほとんどの領域が構築するフレームワークを提供します。

Vueのコア機能

最も基本的なVueは、WebページをJavaScriptと同期します。これを実現するための重要な機能は、リアクティブデータ、およびディレクティブやテンプレート構文などのテンプレート機能です。これらは、初日に学ぶべきことです。

最初のVueアプリを作成するには、WebページにVueをインストールする方法を知って、Vueインスタンスのライフサイクルを理解する必要もあります。

コンポーネント

Vueコンポーネントは、再利用可能な分離されたUI要素です。コンポーネントの宣言方法、およびプロパティとイベントを介してコンポーネント間の通信方法を理解する必要があります。

また、コンポーネントで構成する方法を学ぶことも重要です。これは、Vueで堅牢でスケーラブルなアプリケーションを構築するための基本です。

シングルページアプリケーション

シングルページアプリケーション(SPA)アーキテクチャにより、ユーザーが移動するたびにページを再読み込みおよび再構築するという非効率性なしに、単一のWebページを従来のマルチページWebサイトのように動作させることができます。

Vueコンポーネントとして「ページ」を作成したら、Vueチームが管理するSPAを構築するためのツールであるVue Routerを使用して、それぞれを一意のパスにマッピングできます。

2.現実世界のVue

パート1で得たすべての知識を使用して、ローカルサーバー上であっても、高性能で効率的なVueアプリを構築できます。しかし、本番環境ではどのように立ち上がるのでしょうか?

Vue.jsベースの製品を実際のユーザーに出荷する場合は、さらに知っておく必要があります!

プロジェクトの足場

Vueアプリを頻繁にビルドすると、ほとんどすべてのプロジェクトに戻ってくる設定、セットアップ、開発者ツールがあります。

Vueチームは、Vue CLIと呼ばれるツールを維持しており、これにより数分で堅牢なVue開発環境を構築できます。

フルスタック/認証済みアプリ

Real Vueアプリは通常、データ駆動型のユーザーインターフェイスです。データは多くの場合、Node、Laravel、Rails、Django、またはその他のサーバーフレームワークで作成された安全なAPIから供給されます。

おそらく、データは従来のREST APIまたはGraphQLによって提供されるか、Webソケットを介したリアルタイムデータになる可能性があります。

また、Vueをフルスタック構成に統合するために一般的に使用される設計パターン、およびVueアプリのユーザーデータを安全に保つためのさまざまな考慮事項についても理解する必要があります。

次のVueアプリに最適なバックエンドを決定しようとしている場合は、この記事をご覧ください。

テスト

本番環境で保守可能で安定したVueアプリを作成したい場合、本当にテストを提供する必要があります。

Vueアプリでは、ユニットテストにより、コンポーネントが特定の入力(小道具やユーザー入力)に対して常に同じ出力(つまり、再レンダリングされたHTMLまたは放出されたイベント)を提供することが保証されます。
Vueチームは、Vue Test Utilsと呼ばれるツールを維持します。このツールを使用すると、分離されたVueコンポーネントでテストを作成および実行できます。

Vueとは特に関係ありませんが、E2Eテストはプロジェクトの堅牢性も提供します。 Vue CLI 3を介してプロジェクトに追加できる優れたE2Eツールはサイプレスです。

最適化

アプリをリモートサーバーに展開し、ユーザーが低速の接続を介してアプリにアクセスすると、開発中のテストで経験した速度と効率が得られません。

Vueアプリを最適化するために、サーバー側レンダリングを含むさまざまな手法を使用できます。これは、Vueアプリがサーバーで実行され、出力がユーザーに配信されるHTMLページにキャプチャされる場所です。

最適化のための他の手法には、非同期コンポーネントとレンダリング関数の使用が含まれます。
基本的な機能を学び、無料の2時間のビデオコースBuild Your First Vue.js Appで実際のWebアプリを構築して、Vueを使い始めましょう。

3.主要な関連ツール

これまで見てきたことはすべて、Vue.jsコアまたはエコシステムのツールからのものです。しかし、Vueは単独では存在しません。フロントエンドスタックの1つのレイヤーにすぎません。

上級Vue開発者は、Vueだけでなく、すべてのVueベースのプロジェクトの一部となる主要なツールに精通している必要があります。

モダーンJavaScriptとBabel

Vueアプリは、既存のほとんどすべてのブラウザーがサポートするJavaScriptの標準であるES5で効果的に構築できます。

Vue開発エクスペリエンスを強化し、新しいブラウザー機能を活用するために、最新のJavaScript標準ES2015の機能とES2016以降の提案機能を使用してVueアプリを構築できます。

ただし、最新のJavaScriptを使用することを選択した場合は、古いブラウザーをサポートする方法が必要になります。そうしないと、ほとんどのユーザーが製品を使用できません。

これを達成するためのツールはバベルです。その仕事は、アプリを出荷する前に、最新の機能を標準機能に「トランスパイル」(翻訳およびコンパイル)することです。

Webpack

Webpackはモジュールバンドラーです。つまり、コードが異なるモジュール(たとえば、異なるJavaScriptファイル)にまたがって書かれている場合、Webpackはこれらをブラウザーで読み取り可能な1つのファイルに「ビルド」できます。

また、Webpackはビルドパイプラインとしても機能するため、Babel、Sass、TypeScriptなどを使用して、ビルドする前にコードを変換したり、一連のプラグインでアプリを最適化することもできます。

多くの開発者はWebpackを把握するのが難しく、設定するのがさらに難しいと感じていますが、Webpackがなければ、単一ファイルコンポーネントのようなVueの最高の機能の一部にアクセスできません。

最近リリースされたVue CLI 3では、VueプロジェクトでWebpackを抽象化し、自動的に構成するためのソリューションが提供されています。
これは、学ぶ必要がないということですか? Webpackの構成をカスタマイズまたはデバッグする必要がある場合が必ずあるので、私はノーと言います。

TypeScript

TypeScriptは、タイプ(文字列、ブール、数値など)を含むJavaScript言語のスーパーセットです。これの目的は、堅牢なコードを記述し、バグを早期に発見できるようにすることです。

2019年に登場するVue.js 3は、すべてTypeScriptで記述されます。これは、Vueプロジェクトで使用する必要があるという意味ではありませんが、Vueに貢献してその内部動作を理解する場合は、TypeScriptを理解する必要があります。

4. Vueフレームワーク

フレームワークはVueの上に構築されているため、サーバー側のレンダリングをゼロから実装したり、独自のコンポーネントライブラリを作成したり、その他の多くの一般的なタスクを行う必要がありません。

多くの優れたVueフレームワークがありますが、ここでは最も広く使用され重要な3つのフレームワークについて説明します。

Nuxt.js

高性能のVueアプリを構築する場合は、もちろん、コンポーネントベースのルーティング、サーバー側のレンダリング、コード分割、その他の最先端機能が必要になります。また、SEOタグなどの便利な制作機能も必要になります
Nuxt.jsフレームワークは、このすぐに使用できるすべての機能と、さまざまなコミュニティプラグインを通じて、PWAなどのさらに多くの機能のオプションを提供します。

Nuxt.jsサイトの良い例をご覧になりたい場合は、今すぐご覧ください?

検証

Googleのマテリアルデザイン標準は、美しく論理的なユーザーインターフェースを構築するために広く使用されているガイドラインのシステムであり、AndroidのようなGoogleの製品やWeb全体で使用されています。

Vuetifyフレームワークは、一連のVueコンポーネントでマテリアルデザインを実装します。これにより、マテリアルデザインレイアウトとスタイリングに加えて、モーダル、アラート、ナビゲーションバー、ページネーションなどのウィジェットを備えたVueアプリをすばやく構築できます。

NativeScript-Vue

Vue.jsは、Webユーザーインターフェイスを構築するためのライブラリです。ネイティブのモバイルインターフェースに使用したい場合は、NativeScript-Vueフレームワークで使用できます。

NativeScriptはiOSおよびAndroidのネイティブユーザーインターフェイスコンポーネントを使用してアプリを構築するためのシステムであり、NativeScript-VueはNativeScriptの上にあるフレームワークであり、Vue構文とコンポーネントの使用を提供します。

5.その他

この最後のセクションでは、重要であるが重要ではないトピックまたは上記のカテゴリに当てはまらないトピックについて説明します。

プラグイン開発

プロジェクト全体でVue機能を再利用したり、Vueエコシステムに貢献したい場合は、Vueプラグインとしてインストール可能な機能を作成できます。

プラグインはVueコアの機能ですが、移植可能なVueコードの作成に役立つさまざまなツールと定型文もあります。

アニメーション

アニメーションが必要な場合は、Vueの移行システムも確認してください。これもVueコアの一部です。トランジションを使用すると、DOMに要素を追加または削除するたびにアニメーションを適用できます。

トランジションを行うには、CSSクラスを作成して、フェードイン、色の変更など、目的のアニメーション効果を定義します。 Vueは、要素がDOMに追加または削除されたことを検出し、移行中に適切なクラスを追加または削除します。

プログレッシブWebアプリ

プログレッシブWebアプリ(PWA)は通常のWebアプリに似ていますが、ユーザーエクスペリエンスを向上させる最新の機能で強化されています。たとえば、PWAにはオフラインキャッシュ、サーバーレンダリング、プッシュ通知などが含まれる場合があります。

ほとんどのPWA機能は、Vue CLI 3プラグインまたはNuxt.jsのようなフレームワークを使用してVueアプリに簡単に追加できますが、Webアプリマニフェストやサービスワーカーなどの主要なテクノロジーを理解する必要があります。

私はアンソニーであり、オーストラリアのシドニー出身のWeb開発者です(ただし、海外に旅行してリモートで作業することがよくあります)。

フルスタックVue.js 2およびLaravel 5(2017、Packt Publishing)の著者であり、Ultimate Vue.js Developersビデオコースであり、Vue.js Developers Newsletterのキュレーターです。

2020年に上級Vue開発者になります。
最新のコースで、フルスタックVueアプリの構築、テスト、展開について専門家が知っていることを学び、習得してください。

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

Mohoから出力したSVGを制御したい妄想の話

現在、実際に作ってる最中のブツがこちら。
適当なタイミングで更新をしてるので、ここで書いてる内容から構成が変わってる可能性あり。

【GitHub】SourceOf0-HTML/path_control: SVGを制御したい願望
https://github.com/SourceOf0-HTML/path_control

【GitHub Pages】ベクターデータをいじり倒したい気持ち
https://sourceof0-html.github.io/path_control/

さて。
妄想話の前に、そもそもの話から。

Mohoとは

2Dアニメーションソフトウェアのこと。
昔はAnime Studioという名前だったらしい。
自分が使用しているのはMohoPro12。

2Dアニメーションソフトウェア – Moho (Anime Studio): イーフロンティア
https://www.e-frontier.com/smithmicro/moho/

初めて使ってみたときに調べたことを解説動画として雑にまとめたこともある。

MohoPro12使ってみたので自己流解説 - YouTube
https://www.youtube.com/watch?v=hNsmKullIvo

SVGとは

スケーラブル・ベクター・グラフィックス(Scalable Vector Graphics)の略称であり、画像形式の一種。

PNGやJPGはラスタ形式…ビットマップ画像とも言われるもので、pixel(画素)の集まりで構成されている。
ちなみにpixelはpicture cell(画像の細胞)からの造語らしいよ。
写真なんかは基本ラスタ形式。
細かい色合いを表現できるのが特徴。
拡縮すると劣化するのが弱点。

これに対してSVGはベクタ形式と呼ばれる画像…
というか図形と言った方が分かりやすいかもしれない。
複数の点(アンカーポイント)を繋いだ線(セグメント)に対して、線の太さや塗りの色を設定することで画像が構成されている。
ポスターなんかは基本ベクタ形式。
拡縮しても劣化しないのが特徴。
細かい色合いを表現しようとすると、点と線の数が膨大(≒データサイズが膨大)になるのが弱点。

最近のWeb上のアイコンはSVG形式のものが多い…
てか今見たらQiitaのロゴもSVGだね。

MohoからSVGを出力すると…??

Mohoで作成したアニメーションは、SVGの連番ファイルとして出力することができる。
…が、しかし。

 バ グ る & フ ァ イ ル が 重 い 。

というところは、去年苦労した部分の話。
当時そのあたりで奮闘した話をブロマガでしてたので、そこから引用をば。

【2019-03-06】「SVGでアニメーションさせたいんじゃ」の詳細報告:変人のブロマガ - ブロマガ
https://ch.nicovideo.jp/FlyingEchidna/blomaga/ar1708679

実際にsvgを表示したときに、設定していた一部のマスクが効いてない。
詳しく差分データを用意してタグを解読していくと…
どうやら、マスク設定を有効にしたグループ内にグループを入れ子で置いた場合、マスク設定が出力されない模様。
mp4で出力したときは問題なかったし、Moho側のバグっぽい。

オマケにタグをよく見ると、表示しているパスをそのままマスクとしても流用する設定を入れていた場合、内容が全く同じパスを表示用とマスク用として出力していることが発覚。
どうにかならんかとsvgの仕様を調べたところ、useタグを使うことでパス情報の使いまわしができることが発覚。
どうにかしてuseタグに移行させたい。

…とまあそんなことがありましたとさ。

愚直にSVG連番ファイルを再生

当時、四苦八苦してRubyで

  • バグで吹っ飛んだマスクを、レイヤー名(SVG側でのgタグのID)を頼りに再設定
  • 同じデータを共通設定として整理しなおしてSVGを圧縮

をしまして。
出来上がったSVG連番を、JavaScriptを使ってHTML上でパラパラ漫画形式で再生、というのをやりましたとさ。

【GitHub】SourceOf0-HTML/walk_anime: svgアニメーションのテスト
https://github.com/SourceOf0-HTML/walk_anime

※圧縮したとはいえ、ゲロ重注意(23.1MB)
【GitHub Pages】walk
https://sourceof0-html.github.io/walk_anime/

ただ、このとき書いたRubyのコードは、あまりに四苦八苦したせいか、GitHubには上げてなかったようで。
またSVGをいじるにあたり、今回のGitHubのリポジトリには入ってます。えぇ。

【GitHub】path_control/addMaskTag.rb at master · SourceOf0-HTML/path_control
https://github.com/SourceOf0-HTML/path_control/blob/master/convert/addMaskTag.rb

ものとしては
同階層にsourceフォルダを作成&Mohoから出力したSVG連番ファイルを入れて実行すると、
同階層のdestinationフォルダに同名のSVGファイルを出力する、
というもの。
ただ、さすがに変換前後のSVGまでは重いので、フォルダも含め一緒には上げてないです。

再生だけじゃなく制御したくなってきた

ここからが今年の話。

久々に衝動のままMohoでアニメーションを作っていたところ、どうにも思った動きをしない部分が出てきて、気が付いた。

「あぁ、Mohoがやってるのはボーン(動きを制御するための骨のようなもの)を
基準としたアンカーポイント周りのアフィン変換(移動拡縮回転)でしかないのか」

「…ということは、
インバースキネマティクス(雑に言うと、目標点に向かって腕を伸ばすときの関節の角度の制御方法)で、
ボーンを再現した上で、SVGの制御したら…
Moho上での作業画面みたいな感じで、リアルタイムでアニメーション生成できるんじゃね?」

「Moho上のアクションの合成でやるモーフィング(ある形からある形へ徐々に変形するアニメーション)も実装できたら、夢がひろがりんぐじゃね?」

「やったろ」

尚、この思い付きから、現在すでに1ヶ月経っております。えぇ。
そして、今からするのはあくまで経過報告です。
まだできてない。うん。

ちなみにインバースキネマティクス自体は、p5.jsでシンプルなものを作ったことがあって、実物で言うとこんな感じ。

sketch_190830b - OpenProcessing
https://www.openprocessing.org/sketch/748924

ちなみにフォーワードキネマティクス(指定した角度に関節を曲げる&次の関節にも角度が影響する)というものも存在する。

sketch_190830a - OpenProcessing
https://www.openprocessing.org/sketch/748921

フォーワードキネマティクスの方がシンプルだし、処理的にもよく見かけるかも。
多分ボーンを実装するときはこっちもお世話になる。

もう脱SVGしてバイナリ化しようぜ

今回やりたいことをやろうとすると、SVGの中身を参考にしつつ、どのパーツをどういう形で描画するのか制御しなきゃいけない。
つまり、SVGをそのままHTML上に表示するのではなく、JavaScriptで編集した後のものを表示しなきゃいけない。

そこまでするなら、SVGを解析&座標や色・差分の情報を自分で設計したクラスにぶちこんで、canvasに描画した方がよくない??

というかそこまで解析してしまうなら、それをベースにバイナリ出力&バイナリ読み込みした方が、ファイルサイズも減るやん??

…と、いうことで。
現状、ここまでができており、冒頭に置いてるものは(2020-02-12現在)バイナリ化したものを読み込んで表示しております。
一応改めてリンク張っとこう。

通信量は4.1M。
前述のが23.1MBだったことを思えば、軽い軽い。

【GitHub Pages】ベクターデータをいじり倒したい気持ち
https://sourceof0-html.github.io/path_control/

解説とかこれからの方針とか

作ったブツの構成は現状こんな感じ。

【GitHub】SourceOf0-HTML/path_control: SVGを制御したい願望
https://github.com/SourceOf0-HTML/path_control

ファイル構成(2020-02-12現在)

ファイル・フォルダ 説明
convert/ 前述のSVG変換用のRubyとか入ってる
img/ convertのヤツで変換した後のSVG連番ファイルが入ってる
js/ JavaScriptファイルあれこれ(後述)
src/ 独自形式バイナリファイルが入ってる
BinaryStructuresNote.txt バイナリファイルの構成メモ
ClassStructuresNote.txt クラスの構成メモ
index.html GitHub Pagesで表示してるページ
output_data.html SVG->バイナリ変換処理用HTML
svg.html SVGそのまま再生用HTML

jsフォルダの中身はこんな感じ

ファイル 説明
index.js index.html用
output_data.js output_data.html用
path_control.js メイン処理用
path_control.min.js メイン処理用(Minify済み)
svg.js svg.html用

ちなみにMinifyってのは、ブラウザに読み込ませるJavaScriptのコードが長すぎるとデータが重くなるから、その分通信量かかるでしょ?ってことで、コードを圧縮すること。
人によってはよく「○○.min.js」ってのを見かけると思うけど、あれはそういうもの。

Minify化するツールは検索したらいろいろ出てくるよ。
自分が今お世話になってるのはココ。

JavaScript Minifier
https://javascript-minifier.com/

htmlに対応したjs共

index.js、output_data.js、svg.js、のことね。
主にHTMLで表示しているcanvasの制御と、
path_control.jsで実装してある処理の呼び出しをしてる。
フレームレートやフレーム数を管理しつつ、描画処理命令を呼び出して…みたいな。
この辺りはまだまだ構成をいじるだろうなと思ってる。

というのも、
表示したいものをループ再生させるだの、
ボーンをどう動かすかだの、
どれぐらいモーフィングさせるかだの、
再生周りの制御を最終的にすることになるので…
初期化処理のつもりで作ってるところで、そこまで面倒見てたらえらいことになるやん?

なので、フレーム数処理あたりは特に、メイン処理にあたるpath_control.jsに引っ越しなりなんなりすると思う。
最終的には初期化と、マウスやスマホでの操作の入力を受け取るだけにしたい。

path_control.js

メイン。本題。本体。うん。
久々にガチめのクラス設計をしてるので、クラスの説明をば。

クラス名 説明
PathCtr ファクトリー&シングルトンなクラス
PathContainer 1キャラ分のデータに相当。
画像サイズや各アクションの総フレーム数、GroupObjのインスタンスをリストで持ってる。
GroupObj Mohoのグループレイヤー、SVGのgタグに相当。
PathObjのインスタンスのリストや、入れ子になってるGroupObjのインスタンスのリストを持ってる。
PathObj Mohoのベクターレイヤー、SVGのpathタグに相当。
実際に描画するアンカーポイントやセグメントの塗りの色情報なんかを持ってる。

『ファクトリー&シングルトン』てなんやねん。
いや、あれですわ。
デザインパターンってヤツです。えぇ。

ファクトリーパターンは、生成するときにしか用事がない処理をまとめて実装してある設計のこと。
生成し終わったインスタンスでやる処理と言えば、
描画前の座標更新だったり、
実際の描画処理だったりするんだけど、
生成するときの…
今回だとSVGを解析して~とかバイナリを解析して~なんて処理は、
各々のインスタンスに持たせる必要がない処理なので、
PathCtrにまとめてある。

シングルトンパターンは、実行中のプログラムの中で1つしか実体(インスタンス)を用意しない設計のこと。
C++とかでガチでやるなら、外部からのコンストラクタやコピーコンストラクタの呼び出しを禁止して、別途インスタンスを生成して返すstaticの関数を作って…とかガッツリやることがあったりするけども…

今回JavaScriptでやってるのは、varで用意した変数にオブジェクトを突っ込んでるだけの雑な作りです。
なので、最早クラスと呼んでいいのか怪しい。
単なるオブジェクト。うん。

実際のコードを概要で書くと…

var PathCtr = {
  /** 共通して使いたい変数とか定数とか~ **/
  defaultActionName : "base",  // default action name
  initTarget: null,  // instance to be initialized
  currentFrame : 0,  // current frame
  currentActionID : -1,  // current action ID
  binDataPosRange : 20000, // correction value of coordinates when saving to binary data

  /** 他のクラスのコンストラクタとか~ **/
  PathObj: function(pathDataList, maskIdToUse, fillRule, fillStyle, lineWidth, strokeStyle) {
    this.maskIdToUse = maskIdToUse;    // ID of the mask to use
    this.pathDataList = pathDataList;  // path data array
    this.fillRule = fillRule;          // "nonzero" or "evenodd"
    this.fillStyle = fillStyle;        // fillColor ( context2D.fillStyle )
    this.lineWidth = lineWidth;        // strokeWidth ( context2D.lineWidth )
    this.strokeStyle = strokeStyle;    // strokeColor ( context2D.strokeStyle )
    this.hasActionList = [];           // if true, have action
  },

  /**
  インスタンスを生成して返すfunctionとか、
  生成する中で使いまわすために分けたくなったfunctionとか。 
  **/
  getMaskId: function(maskStr) {
    /** 今作成中のPathContainerのインスタンスから、このマスクのグループIDよこせ~処理 略 **/
  },
  dataTobin: function(pathContainer) {
    /**
    このPathContainerのインスタンスを
    バイナリにしてよこせ~処理
      略
    **/
  },
  svgFilesLoad: function(fileInfoList, completeFunc) {
    /**
    fileInfoListで指定してあるファイルを読み込んで、
    読み込み完了したら、生成したPathContainerのインスタンスを
    completeFuncの引数にして実行(コールバック)しろ~処理
      略
    **/
  },
  /** などなど **/
};

といった感じ。

今後ボーンを実装するにあたり…

ボーン周りは現状まったく未着手。
その前にアフィン変換部分を実装しようかと思ってる。
ようは行列変換。そう。数学で出てくるあの行列。

\begin{pmatrix}
y_1 \\
y_2 \\
1
\end{pmatrix}
=
\begin{pmatrix}
p & q & b_1 \\
r & s & b_2 \\
0 & 0 & 1
\end{pmatrix}
\begin{pmatrix}
x_1 \\
x_2 \\
1
\end{pmatrix}

↑ こういうやつ。
数学赤点偏差値28の自分がそんな高等なことできるの?
って言われたら…
まあ…自力で全部理屈から追って実装するのはキツいかな。

ただ、世の中便利なもので、そういう行列変換周りの処理をまとめて実装してあるコードは存在して、自分も実際お世話になりまくってるのよね。
描画系のプログラムを書いたことがある人なら見たことがあるかもしれない。

xxx.translate();
xxx.rotate();
xxx.scale();

こういう関数。ようはこれの実装だったりする。
それでいて、これの実装をしてる人は世の中に結構いるわけで。

今回は ↓ を参考にさせていただいて実装して、描画前の更新処理として走らせようかなと思ふ。

【GitHub】transformation-matrix-js/matrix.js at master · leeoniya/transformation-matrix-js
https://github.com/leeoniya/transformation-matrix-js/blob/master/src/matrix.js

と、いったところで。
現状報告終了!

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

JavaScriptしか知らない初心者のReact Native入門

プログラミング歴半年の素人が書いています。

間違いのないようご自身でも良く調べた上でお願いいたします。

対象読者

  • モバイルアプリ開発をしたことがない人
  • JavaScriptの基礎構文がわかる人
  • Reactを知っている人
  • Macの人

わたしです。 試行錯誤の記録となっておりますので、ベストプラクティスではない可能性が十分含まれておりますことご了承ください。

環境構築

Node.jsのインストール

https://nodejs.org/en/download/

上記リンクからダウンロードできます。

ReactNativeのインストール

公式チュートリアルでもおすすめされているように、「Expo CLI」という「ReactNative+便利機能いろいろ」のパッケージをインストールしてみます。

ついでに、お手持ちのスマホのアプリストアで「Expo」を検索し、アプリをスマホにダウンロードしておくとよいでしょう。自分のスマホで、開発中の画面を確認することができます。

$ npm install -g expo-cli

上記のコマンドで「Expo CLI」をインストールできます。

新しいプロジェクトの作成

早速、アプリの開発にとりくみましょう。

$ expo init AwesomeProject

新しいプロジェクトを作成します。今回は「AwesomeProject」という名前をつけました。

途中、スターターテンプレートを何にするか聞かれます。
「Blank」とか「Blank with TypeScript」だとか、いろいろありますが、今回は初めてなので「Blank」を選択してみました。

cd AwesomeProject
npm start

プロジェクトの作成が済んだら、作成したディレクトリに移動して、npm startで起動します。
ブラウザにCLIが表示されたら成功です。

Expoのダウンロードと登録

https://expo.io/

上記からExpoにアクセスし、SignUp します。
お使いのスマホがあれば、公式アプリをダウンロードします。

Expoで開発画面を表示する

npm startしてブラウザに表示された画面に、QRコードがあればそれをスマホで読み取ることで開発中の画面を表示することができます。

ここでエラー!

私の場合はここでエラーがでました。

error Unable to resolve "@react-navigation/native" from "App.js"

うーん、初めてなのでなんのこっちゃモジュール。

ひとまずApp.js公式チュートリアルのとおり、シンプルに書き直すことで解決しました。

App.js
import React, { Component } from 'react';
import { Text, View } from 'react-native';

export default class HelloWorldApp extends Component {
  render() {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <Text>Hello, world!</Text>
      </View>
    );
  }
}

ちなみに、Hooksで関数コンポーネントにしても動きました。(react-native: 0.61.4

App.js
import React from 'react';
import { Text, View } from 'react-native';

const HelloWorldApp = () => {
    return(
        <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
            <Text>Hello, World!</Text>
        </View>
    )
}

export default HelloWorldApp

無事、Hello Worldまでたどりつきました。

スクリーンショット 2020-02-12 21.21.43.png

React Native独自のコンポーネント

通常のJSXで使うことができる<div><h1>などのコンポーネントの代わりに<View><Text>を使います。

- <View> : <div><span>
- <Text> : <p>など文字列の表示に使用

画像を表示する

「Hello, World!」の代わりに画像を表示します。
React Nativeでは<img>のかわりに<Image>が用意されているので、インポートして使います。

画像ソースを指定するプロパティは、srcではなくsourceです。

App.js
import React from 'react';
// Imageコンポーネントをインポート
import { Text, View, Image } from 'react-native';

const HelloWorldApp = () => {
    // 画像へのパスを定義
    let pic = {
        uri: 'https://i.picsum.photos/id/237/300/200.jpg'
    }

    return(
        <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
            <Image source={pic} style={{width: 300, height: 200}} />
        </View>
    )
}

export default HelloWorldApp

PropsとStateは通常のReactと同じ

同じ...だと思います。

まとめ

...更新中

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

【初心者向け】JavaScriptのfor文でHTMLに表示する九九表を作る!

JavaScriptでHTMLに表示するtableを作る!

スクリーンショット 2020-02-12 20.23.01.png

完成形はこんな感じ:baby_tone2:
codepenから直接コード見られます。

tableを作るのに使う要素

HTML

  • <body></body>
  • <table></table>

JavaScript

  • document.write('xx')
    • <tr></tr>
    • <th></th>
    • <td></td>
  • for(var hoge=1; hoge <= 9; hoge++){document.write('<xx>'+ hoge +' </xx>');}

'<xx></xx>'は繰り返すtr,th,tdが入ります。
hogeは仮の名前です。特に意味のないclassや変数名をつける時使います。

実際のコード

そのコードでどのようなことがしたいのかがわかるよう、かなりコード内で説明してます。

先に備考
* <table></table>のタグはあえてscriptタグに入れていません。<table><script>...<script></table>こんな構成
* i,j,kは変数名。仮の文字なので実行したいことがわかればいい
* xは横の行で計算をしていることをわかりやすくするため仮に置いている文字
* yは縦の列で計算をしていることをわかりやすくするため仮に置いている文字

HTMLとJavaScript

  <body>
  <div class="container">
    <h1>九九の表を作る</h1>
    <table class="border" border="1">
      <script>
        //xの行を作る
        document.write('<tr>');

        //何も掛け算をしない空白のthを作成
        document.write('<th></th>');

        //xの行を記述するため、1から9の数字を繰り返しHTMLに記述する
        for(var i=1; i <= 9; i++){
          document.write('<th>'+ i +' x</th>');
        }

        //ここでxの行を閉じる
        document.write('</tr>');

        //ここまでxの行

        //jの列を作る
        document.write('<tr>');

        //jの列を記述するため、1から9の数字を繰り返しHTMLに記述する
        for(var j=1; j <= 9; j++){
          document.write('<th>' + j + 'y</th>');

        //j*kを繰り返すことで、計算結果をtdタグの中に表示する
          for(var k=1; k <=9; k++){
            document.write('<td>'+ j*k +'</td>');
          }
        //ここでjの行を閉じる
          document.write('</tr>');
        }
      </script>
    </table>
  </div>
  </body>

CSS

    @charset 'utf-8';

    ./*layout
    -----------------------------------*/
    .container{
      margin: 0 auto;
      padding:0;
      box-sizing:border-box;
      background-color:rgba(255, 255, 255, 0.7);}
      .border{
        border-collapse: collapse;
        border: 1px solid #333;
      }
      th,td{
        width: 40px;
        height: 40px;
        text-align: center;
      }
      th{
        background-color:#ff525f;
      }
      td{
        background-color:#fffffb;
      }
    </style>

ちょっと解説?

1番目のfor文でやってること

スクリーンショット 2020-02-12 20.57.01.png
この行を作ってる

2,3番目のfor文でやってること

スクリーンショット 2020-02-12 20.59.00.png
要はこの行の掛け算( j*k )を9回繰り返して下の画像のようにしてる
スクリーンショット 2020-02-12 20.57.34.png

おわりに

雰囲気for文を使った掛け算のやり方がわかったでしょうか、、?

htmlでテーブルを作ろうとすると逆にtrの中にth,tdをどのように配置すればいいのかわからないことがあるので繰り返しのテーブルを作る際にはJavaScriptの方がわかりやすいこともあるかもしれないですね!

日常でこの九九表を作ることに意味は感じませんが、for文の中にfor文があるという時の練習にはなるのではないでしょうか。。

私もよくわかってませんが、何かの参考になれば幸いです!(雰囲気なので間違っていたりもっといい伝え方があれば教えていただきたいです、、ごめんなさい、、、)

今後、計算結果で偶数の時は色を変える、、の処理もいつか記事にしたいと思います:wave:

お疲れ様でした!

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

自分用コードメモ - 特定のCookieを持つかどうか判別して処理をする

よくある、特定のCookieを持つかどうかで処理を分けたいときに使うコード。例えば特定のアンケートを回答した人には同じアンケートを表示しないなどといった運用をするときに使う。

//メイン. 特定のCookieを持たなけれな特定の処理をしてCookieを付与する
function main(cookieName,cookieValue){
  if(!existCookie(cookieName,cookieValue)){
    myFunciton();
    document.cookie = cookieName + "=" + cookieValue;
  }
}

//特定のCookie名を持たないブラウザで処理したい内容
function myFunciton(){
  ...
  //console.log("TEST");
}

function existCookie(cookieName,cookieValue){ 
  //cookieName=任意のCookie名 cookieValue=そのCookie名を持たないときに処理した後セットしたい値
  var getCookieData = document.cookie.split(';');
  var existFlag = false;
  for (var i=0;i<getCookieData.length;i++){
    var target = getCookieData[i].split('=');
    if(target[0].replace(' ','')==cookieName){
      return = true;
    }
  }
}

↓ チェック・処理実行

main("testCookieName","testCookieValue");

もっと洗練された方法があれば、細かい指摘でも結構なのでコメントください!!

\ Follow Me! /
Qiita アカウント
Twitter アカウント

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

自分用コードメモ -特定のCookieを持つかどうか判別して処理をする

特定のCookieを持つかどうかで処理を分けるときに使う、珍しくもないよくあるコード

//特定のCookie名を持たないブラウザで処理したい内容
function myFunciton(){
  console.log("TEST");
}

function existCookie(cookieName,cookieValue){ 
  //cookieName=任意のCookie名 cookieValue=そのCookie名を持たないときに処理した後セットしたい値
  var getCookieData = document.cookie.split(';');
  var existFlag = false;
  for (var i=0;i<getCookieDataSet.length;i++){
    var target = getCookieDataSet[i].split('=');
    if(target[0].replace(' ','')==cookieName){
      existFlag = true;
      break;
    }
  }
  if(!existFlag){
    myFunciton();
    document.cookie = cookieName + "=" + cookieValue;
  }
}

↓ チェック・処理実行

existCookie("myName","myValue");

もっと洗練された方法があれば、細かい指摘でも結構なのでコメントください!!

\ Follow Me! /
Qiita アカウント
Twitter アカウント

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

すっきり書きたい JavaScriptの条件分岐

はじめに

未経験からNode.jsの現場に配属された2019年新卒エンジニアが、学習の振り返りとしてJavaScriptの基礎の基礎をまとめます。

過去のJavaScript基礎シリーズ↓
JavaScriptでvarが推奨されない理由を整理してみた

今回は、多くの書き方が存在するJavaScriptの条件分岐に関して、よりすっきりとした書き方を考えていきます。

Goal

  • 思考停止のelseやswitchから離れる
  • 可読性やリファクタリングのしやすさの観点から、JavaScriptの条件分岐を使いこなす

まず「すっきり」を定義する

本記事で目指したい「すっきり」を、以下のように定義します。

  • コードの可読性が高いこと
  • バグが生まれにくいこと
  • 後からリファクタリングがしやすいこと

コードの可読性が高いこと

プロジェクトとして複数人で開発をする際に、可読性の高いコードを書くことはとても重要です。

「可読性」という言葉の意味するところは本記事では割愛しますが、こちらの記事がわかりやすくとても参考になるので、詳しく掘り下げたい方は併せてお読みください。

可読性については、一人で自習ばかりしているとおろそかになってしまいがちな考え方なので、常に考えておく癖をつけておきたいですね。

バグが生まれにくいこと

これは、書き忘れや書き間違いによるバグが発生しにくい、といった意味合いです。期待していない動作の場合に、エラーとして表示される等でバグに気づきやすい書き方は、未然にバグを防げます。

後からリファクタリングがしやすいこと

後から仕様の追加や変更があった際に、対応しやすいコーディングを指します。

ピンとこない方はこちらの記事が、リファクタリングの重要性を分かりやすくまとめていただいていますので、ぜひお読みください。

JavaScriptの条件分岐をおさらい

ということで、「可読性が高く」「バグが生まれにくく」「後からリファクタリングがしやすい」条件分岐の書き方を考えていきたいと思うのですが、その前にまず、JavaScriptの条件分岐に用いられる基本的な構文を整理します。

基本的な書き方

  • else文
  • if...else文
  • switch文

やや応用的な書き方

  • 三項演算子
  • 短絡演算子

基本的な書き方

else文

const pokemon = {
  name: "ヤドン"
};

const isGalar = pokemon => {
  if (pokemon.name === "ヤドン") {
    return true;
  } else {
    return false;
  }
};

console.log(isGalar(pokemon));  // ログの出力結果は"true"

多くのプログラミング教材で最初に習う条件分岐が、おそらくこの構文ではないかと思われます。if文で条件に合致した場合の式が実行され、合致しなかった場合はelseが実行されます。

ただし、条件に合致しない場合はif文以下の実行が無視されるだけなので、上の例のような単純な真偽判定のみであれば、以下のようにelseを省略することが可能です。

const isGalar = pokemon => {
  if (pokemon.name === "ヤドン") {
    return true;
  } 
  return false;
};

else if...文

const yadon = {
  name: "ヤドン"
};
const galarPokemon = {
  name: "ヌオー",
  galarNumber: 101
};
const kantoPokemon = {
  name: "ヒトデマン"
};

const isGalar = pokemon => {
  if (pokemon.galarNumber) {
    return true;
  } else if (pokemon.name === "ヤドン") {
    return true;
  } else {
    return false;
  }
};

console.log(isGalar(yadon)); // 出力結果は"true"
console.log(isGalar(galarPokemon)); // 出力結果は"true"
console.log(isGalar(kantoPokemon)); // 出力結果は"false"

こちらも、初歩的な条件分岐としてよく出てきますね。3つ以上の条件分岐が必要な場合に活用する構文です。

ただ、JavaScriptに「else if」構文というものはありません。 実際の挙動としてはelse文の中でさらにif節とelse節のネストが生成されているのと等しいようです。
また、この書き方でもelseの省略が可能です。以下のコードは上記の例と同様の実行結果となります。

const isGalar = pokemon => {
  if (pokemon.galarNumber) {
    return true;
  } 
  if (pokemon.name === "ヤドン") {
    return true;
  } 
  return false;
};

switch文

const evolutionEevee = stone => {
  let eevee;
  switch (stone) {
    case "ほのおのいし":
      eeVee = "ブースター";
      break;
    case "みずのいし":
      eevee = "シャワーズ";
      break;
    case "かみなりのいし":
      eeVee = "サンダース";
      break;
    default:
      eevee = "イーブイ";
  }
  return eevee;
};

console.log(evolutionEevee("ほのおのいし")); // 出力結果は"ブースター"
console.log(evolutionEevee("みずのいし")); // 出力結果は"シャワーズ"
console.log(evolutionEevee("かみなりのいし")); // 出力結果は"サンダース"
console.log(evolutionEevee("かたいいし")); // 出力結果は"イーブイ"

こちらも、複数の分岐が発生する条件式における定番の書き方です。case ○○:で条件を分岐させ、いずれのcaseにも一致しなかった場合はdefaultの式が実行されます。

注意すべき点は、case節やdefault節で条件分岐の処理を書いた後のbreakの有無によって、switchの実行が異なるという点です。

switch文は、処理の中でbreakが入力されていた場合に条件の判定をやめ、switch文を抜けて次の文から実行を続けます。

breakが入力されていないと条件に合致したcase節があってもswitch文が継続されてしまうのです。

breakが無い場合の挙動は以下のようになります。

const evolutionKlink = pokemon => {
  let grade;
  switch (true) {
    case /ギギギアル/.test(pokemon):
      grade = 3;
    case /ギギアル/.test(pokemon):
      grade = 2;
    case /ギアル/.test(pokemon):
      grade = 1;
  }
  return grade;
};

console.log(evolutionKlink("ギギギアル"));
console.log(evolutionKlink("ギギアル"));
console.log(evolutionKlink("ギアル"));
// いずれも出力結果は「1」

上の例では、test関数を利用して、引数pokemonを正規表現によって判定する関数を実行しているのですが、breakを書かなかったせいで、手前のcaseに合致する場合でも全てのcaseで判定が行われ、意図しない結果が出力されてしまっています。breakを書けばこのバグは防ぐことができます。

あえてbreakしないswitchの書き方というのもあるようですが、基本的にswitchを使うならbreakを忘れずに書いた方が安心ではないかと思います。

もしくは、case節で直接return文を書き、条件に一致したcase以降の式を読み取らせないようにするのが良いでしょう。

やや応用的な書き方

三項演算子

ifを用いない条件分岐の書き方として、三項演算子というものがあります。
これは、if文でブロックを分けたくない場合に、コンパクトに条件分岐を書くことができるとても便利な構文です。構文は以下の通りです。

条件式 ? 条件がtrueの場合の処理 : 条件がfalseの場合の処理

たとえば以下のように、変数に代入する値を条件分岐で決めたい時に使われることが多いです。

// 例
const pokemon = {
  name: "サニーゴ",
  region: "ガラルちほう"
};

const typeCheck = sunnygo => {
  const type = sunnygo.region === "ガラルちほう" ? "ゴースト" : "みず・いわ";
  return `サニーゴは${type}タイプです`;
};

console.log(typeCheck(pokemon)); // 出力結果は"サニーゴはゴーストタイプです"

三項演算子を使わずにこれを記述すると、以下のようになります。

const pokemon = {
  name: "サニーゴ",
  region: "ガラルちほう"
};

const typeCheck = sunnygo => {
  let type;
  if (sunnygo.region === "ガラルちほう") {
    type = "ゴースト";
  } else {
    type = "みず・いわ";
  }
  return `サニーゴは${type}タイプです`;
};

console.log(typeCheck(pokemon)); // 出力結果は"サニーゴはゴーストタイプです"

else文だと、コードのブロックが増えていたり、関数の上部でletを宣言してから再代入をするような書き方を余儀なくされたり、少し「すっきり」ではなくなっている、という感覚を掴んでいただけるかと思います。

短絡演算子

&& (論理AND) や || (論理OR)といった論理演算子を用いたショートカットの構文も、条件分岐の一種として活用することができます。構文は以下の通りです。

値A || 値B
 値Aがfalseの判定の場合、値Bが判定される 

値A && 値B
 値Aがtrueの判定の場合、値Bが判定される
const letsGo = pokemon => {
  const partner = pokemon || "イーブイ";
  return partner;
};

console.log(letsGo("ピカチュウ")); // 出力結果は"ピカチュウ"
console.log(letsGo(null)); // 出力結果は"イーブイ"

上の例では、引数pokemonがfalsyな値(false, 0, -0, NaN, null, undefined, 空文字列(""))だった場合に、必ず"イーブイ"を戻り値にする関数が実行されています。

条件がfalsyだった場合、デフォルトの返り値を短いコードで表現する際に、よく用いられる書き方です。

この書き方が見慣れない方は、以下のコードを見てもらえば挙動のイメージがつくかと思います。

const letsGo = pokemon => {
  if (!pokemon) {
    return "イーブイ";
  }
  return pokemon;
};

ちなみに、短絡演算子ではfalsyな値を全て一律に評価するので、たとえば引数がundefinedの時のみデフォルトを返すようにしたい、というような場合は、以下のように書く必要があります。

const letsGo = pokemon => {
  const partner = pokemon === undefined ? "イーブイ" : pokemon;
  return partner;
};

console.log(letsGo()); // 出力結果は"イーブイ"
console.log(letsGo(null)); // 出力結果はnull

すっきりな条件分岐を考える

JavaScriptの主な条件分岐の書き方をおさらいしたところで、ここから「可読性が高く」、「バグが生まれにくく」、「後からリファクタリングがしやすい」、すっきりした条件分岐の書き方を追求します。

結論から言うと、本記事の主張は以下の3点です。

  • ショートカット演算子は使いどころをきちんと定め、多用しないようにする
  • 実践のコーディングでelseは使わなくてよい
  • 複数の条件分岐はswitch以外の書き方も覚えておくこと

何でもショートカット演算子、ではNG

三項演算子や短絡演算子は、一行で条件分岐が記述できる便利な構文のため、つい多用してしまいがちになりますが、何にでも使えばいいというわけではなく、かえって可読性を下げてしまう場面もあります。

const sunnygo = {
  region: "カントー地方"
};

const isGalar = sunnygo => {
  return sunnygo.type === "ゴースト" || "" ? true : sunnygo.type = "みず・いわ";
};

isGalar(sunnygo);

console.log(sunnygo.type); // 出力結果は「みず・いわ」

上記のコードでは、引数sunnygoのtypeプロパティが"ゴースト"という文字列か空文字だった場合はtrueを、それ以外の場合はtypeの値を"みず・いわ"にする、というような関数を無理やりショートカット演算子で1行にして記述していますが、何が返ってくる関数なのかが一目で分かりづらいことになっていますね。

(実際にこんなコードを書く人はいないと思いますが、あくまで「やろうと思えばこういうことが出来てしまう」という悪い例なので大目に見てください……。)

入り組んだ条件分岐は、素直にifで書きましょう。

const sunnygo = {
  region: "カントー地方"
};

const isGalar = sunnygo => {
  if (sunnygo.type === "ゴースト" || "") {
    return true;
  }
  return (sunnygo.type = "みず・いわ");
};

isGalar(sunnygo);

console.log(sunnygo.type); // 出力結果は「みず・いわ」``

elseって必要?

個人的に、elseは使わなくても良い、と考えています。私は実務のコーディングでelseを書いたことがありませんが、その理由がelseがどうしても必要になる場面がないからです。

elseは分岐の条件を書かないため、else節の処理の意味がつかみづらくなるという短所があります。単に「if節の条件がfalseの場合」を強調したいなら三項演算子で良く、そうでなくとも、if節でreturn文を書いてif節の外でデフォルトの値をreturnする、という風に書けば良いので、ブロックを増やしてまでelse節を記述する必要はないのです。ifとそれ以外、という条件の分け方を行う場合、else節は無くても大丈夫です。

else ifについても同様で、else ifを使いたい場面では、たいていswitch文を使うか、ifをひとつずつ書いていく方が「すっきり」する場合が多いです。

else文はコーディングの勉強で活用するにとどめ、実践ではelseを書かないクセをつけると良いかと思います。

switchばかり使うのは危険?

複数の条件分岐を書こうというときにswitch文は非常に便利な構文ですが、switchしか解決法を持っていないと、しばしば困る場面も訪れます。

先述のとおり、switchはbreakの仕様のおかげで、気をつけないとバグを生んでしまう構文です。また、3~4ケースの条件分岐ならあまり気になりませんが、分岐が100ケースを超えたりすると、switchだと非常に読みづらいです。

const pokemon = {
  name: "ニャース"
};

const pokemonIndex = pokemon => {
  switch (pokemon.name){
    case "フシギダネ":
      return 1;
    case "フシギソウ":
      return 2;
    case "フシギバナ":
      return 3;
    case "ヒトカゲ":
      return 4;
    case "リザード":
      return 5;
    case "リザードン":
      return 6;
    case "ゼニガメ":
      return 7;
    case "カメール":
      return 8;
    // 以下、図鑑No.151まで続く...
  }
};

上記は、ポケモンの名前に応じた図鑑ナンバーを返すプログラムを書こうとしているところなのですが、switchで151ケース(default含めると152ケース)の条件分岐を行うことがいかに無謀なことなのかは、何となくお分かりいただけるかと思います。。

しかも、この書き方だとポケモンに少し詳しい人じゃないと図鑑ナンバーのことを言っていることが伝わりづらい。。

こうした時のために、switch以外で複数の条件分岐を書く術も、いくつかあると良いでしょう。

私がよく使っているのが、find関数を用いた条件分岐です。

Array.prototype.find
array.find( callback関数() )

これは、配列の要素それぞれに対してコールバック関数を実行し、最初にtrueな値を返すという関数です。もちろん、本来は配列を扱うための関数なのですが、これを活用することで、たとえば以下のように条件分岐を書くことができます。

const pokemonList = [
  { name: "フシギダネ", No: 1 },
  { name: "フシギソウ", No: 2 },
  { name: "フシギバナ", No: 3 },
  { name: "ヒトカゲ", No: 4 },
  { name: "リザード", No: 5 },
  { name: "リザードン", No: 6 },
  { name: "ゼニガメ", No: 7 },
  { name: "カメール", No: 8 },
  { name: "カメックス", No: 9 },
  { name: "キャタピー", No: 10 },
  { name: "トランセル", No: 11 },
  { name: "バタフリー", No: 12 },
  // 以下、図鑑No.151まで続く
];


const pokemon = {
  name: "ニャース"
};

const pokemonIndex = pokemon => {
  const pokemonNumber = pokemonList.find(p => p.name === pokemon.name).No;

  return pokemonNumber;
};

console.log(pokemonIndex(pokemon));  //出力結果は52;

上記のコードでは、配列pokemonListに分岐の対象となっているポケモンの名前と図鑑ナンバーのセットを格納し、関数pokemonIndexで、引数に指定したポケモンの名前とpokemonListに格納されたnameが一致するまで配列内で検索を行い、配列で最初に一致した値を返しています。

switchで書く場合と比較して、objectとして要素を扱うのでコピペがしやすかったり、ページ内検索がかけやすかったりするため、リファクタリングのしやすさが格段に上がっていることがお分かりいただけたら幸いです。これなら、ある日突然、全国図鑑になったとしても充分に対応できます。

まとめ

  • else文:使わなくても平気
  • switch文:便利だけど、ときに不便
  • 三項演算子:変数の代入でサクッと条件分岐したいときに使おう
  • 短絡演算子:デフォルトの値を明示したいときに使おう
  • find関数は良いぞ

JavaScriptには色々な構文がありますが、それぞれの長所・短所を踏まえつつ、すっきりしたコーディングを目指していけると良いのかな、と思います。

以上です。
間違い、不足点の指摘などあれば、コメントをお願いいたします。

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

PlayCanvasで建築系のモデルを良い感じの3Dビュワーにしてみる [WebGL]

PlayCanvasの非ゲーム分野を開拓中のキドです。

今回はビジュアライゼーションで建築系に使えそうなコンテンツを作ってみました。

建築系ビジュアライズ

build.gif

ここでコンテンツを見ることができます
https://playcanv.as/p/F1ZzFOYS/

とあるビルのオフィス内の案内コンテンツ、という感じで作りました。
3Dコンテンツなのでカメラで外装を見たり、階層の番号をクリックすることでその階層の中を一望できたり。
あとはそれに応じたDOMも変更したり、レイキャストでクリックできるスポットを設置したりしました。

FlashがWebでよく使われていた頃に、デパートとか施設のマップ案内をするWebページがありました。
その頃はFlashで実装されていましたが、2020年いっぱいでFlashも終わってしまいます。
今では画像だとかpdfだとかで実装されていて、若干見にくくなった気がしますね…

そんな施設のマップ案内をFlashの代わりになりそうですよ。

作り方

今回の記事では完成したプロジェクトから紹介しているだけなので、丸々コピーしても正しく動作しない場合があるのでご了承を。

また、コードの説明については軽く触れますが詳しく説明しませんのでこちらもご了承を。

レシピ

今回は以下の3つ用意すればおk

PlayCanvasから新規プロジェクトを作る際はテンプレートに「Model Viewer Starter Kit」を選んでください。
スクリーンショット 2020-02-12 15.42.57.png

3Dモデルを配置

用意した3Dモデルは階層ごとにあらかじめ分けておきます。
今回はLightを使用せず、3Dモデルに陰影を焼き付けてから使用しています。

HierarchyでのEntityの配置の仕方は、buildingを親として子に各階層を入れています。
スクリーンショット 2020-02-12 15.50.40.png

3Dモデルが移動する位置を決める

先ほど配置した3DモデルのEntityの子に、横にスライドしたときの位置を決めたEntityを配置します。
同じ3Dモデルを使って位置を決めるとわかりやすくて良いですね。決められたらこのEntityはEnabledをfalseにして非表示しておきます。
スクリーンショット 2020-02-12 15.57.14.png

3Dモデルの配置ができたら、次はCameraも移動する位置を決めます。
横にスライドした3Dモデルを俯瞰できるような位置に設置して、これもEnabledをfalseにして非表示にします。
スクリーンショット 2020-02-12 15.57.24.png

Cameraの設定

テンプレートから作っていれば以下のScriptsがセットされていると思います。

  • orbitCamera.js
  • mouseInput.js
  • touchInput.js

これらはこのままで。
orbitCamera.jsFocus Entityを変更しますが、その原点となるEntityを設置します(2枚目の画像)
ここではorbitcamera-gentenと名前をつけていますが、原点にしか使わないのでなんでも良いです。
Focus Entityが変更できたらOKです。

スクリーンショット 2020-02-12 16.03.29.png
スクリーンショット 2020-02-12 16.03.43.png

autoRotate.js

Cameraに新しくスクリプトを書きます。
自動回転するスクリプトですが、必要なければ無視しても問題ないと思います。

このautoRotate.jsはテンプレートにあるorbitCamera.jsの中のCameraのYawを操作しています。

updateでthis.orbitCamera.yaw += this.speed;とやっているのが、ここでの肝でしょうか。
this.timer += dt;とかはAttributesのwaitと比較していて、急に回転するのを止めてくれています。
ちょっと待ってから自動回転する、そんな感じにしてくれます。
vueAppと見慣れないものがありますが、これは後ほどDOMと連携させる際にVue.jsでコントロールしているのがここでも影響しているためです。

var AutoRotate = pc.createScript('autoRotate');

AutoRotate.attributes.add('speed', {type: 'number',default: 1,title: 'Speed',description: 'The rotate speed of camera.'});

AutoRotate.attributes.add('wait', {type: 'number',default: 5,title: 'Wait',description: 'Enable auto rotate after seconds.'});

AutoRotate.prototype.initialize = function() {
    this.timer = 0;
    this.orbitCamera = this.entity.script.orbitCamera;
};

AutoRotate.prototype.resetTimer = function() {
    this.timer = 0;
};

AutoRotate.prototype.update = function(dt) {
    if(vueApp.rotateFlag) return false;
    if(!vueApp.openfloor&&!vueApp.clickAnimation){
        this.timer += dt;

        if (this.timer > this.wait) {
            this.orbitCamera.yaw += this.speed;
        }
    }
};

DOMを追加

3Dモデルはこれぐらいでだいたいおkなので、HTMLとかCSSの方にいきましょう。

Vue.jsを使う

今回はVue.jsを使うのですが、PlayCanvasでVue.jsとかのCDNを使う場合にはSETTINGSのEXTERNAL SCRIPTSを使います。
スクリーンショット 2020-02-12 16.24.17.png

詳しくは別記事でも説明しているのでそちらから

changeFloor.js

orbitCamera.jsがCameraの動きの中枢でしたが、このchangeFloor.jsはこのコンテンツの柱なので他よりコード多めです。

DOMを配置するためにdiv要素wrapperを配置したり、CSSはgetFileUrl()を使いlink要素で読み込んだりしていますが、
一番の肝は選択された階層に対するCameraと3Dモデルの移動でしょうか。

vueAppの箇所がVue.jsで操作する箇所ですね。
あまりVue.jsの綺麗な書き方ができていませんが許して。

tween()はPlayCanvasのTweenライブラリです。
これを使ってCameraのPositionやAngle、、3DモデルのPositionを移動させています。
CameraのAngleですが、RotationではなくEulerAnglesで変更してあげないと挙動がおかしくなってしまうのでご注意です。

このTweenの処理をtrue/flaseのFlagで管理しています。
階層がOpenなのかCloseなのか見て、それに対応した処理を行なっています。

このFlagは先ほどのautoRotate.jsの以下のコードも見ています。
ここではCloseの状態でTweenのアニメーションが切れていればautoRotateするようにしています。

if(vueApp.rotateFlag) return false;
    if(!vueApp.openfloor&&!vueApp.clickAnimation){
/*jshint esversion: 6, asi: true, laxbreak: true*/
const ChangeFloor = pc.createScript('changeFloor');

ChangeFloor.attributes.add("baseHtml", {type:"asset", assetType:"html"});
ChangeFloor.attributes.add("setCSS", {type:"asset", assetType:"css"});
ChangeFloor.attributes.add("target", {type:"entity"});
ChangeFloor.attributes.add("cameraTarget", {type:"entity"});

ChangeFloor.prototype.initialize = function() {
    let self = this;
    let canvas = document.getElementsByTagName("canvas")[0];
    canvas.classList.add("pcCanvas");
    let wrapper = document.createElement("div");
    wrapper.classList.add("wrapper");
    wrapper.innerHTML = self.baseHtml._resources[0];
    canvas.parentNode.appendChild(wrapper);

    let css = document.createElement("link");
    css.setAttribute("href", this.setCSS.getFileUrl());
    css.setAttribute("rel", "stylesheet");
    document.head.appendChild(css);

    let t_camera = self.cameraTarget;
    let cameraPosOri = Object.assign({},t_camera.getLocalPosition());
    let cameraRotOri = Object.assign({},t_camera.getLocalEulerAngles());

    let tHead = "とあるビルのオフィス内";
    let tContent = "PlayCanvasでビル内の各階層を3Dで観ることができます。 <br>気になる箇所をクリックすることで詳細が観れます。";

    vueApp = new Vue({
        el: '#app',
        data: {
            openfloor: false,
            clickAnimation: false,
            floors: self.target.children,
            lastTarget: null,
            targetPosOri: [],
            DOMhead: tHead,
            DOMcontent: tContent,
            DOMraycastFlag: false,
            DOMraycastHead: "",
            DOMraycastContent: "",
            rotateFlag: false,
        },
        methods: {
            onfloorClick: function(target,index) {
                const v_self = this;
                if(v_self.clickAnimation || v_self.openfloor) return;
                cameraPosOri = Object.assign({},t_camera.getLocalPosition());
                cameraRotOri = Object.assign({},t_camera.getLocalEulerAngles());
                v_self.clickAnimation = true;
                v_self.openfloor = true;

                v_self.lastTarget = index;
                v_self.targetPosOri[index] = Object.assign({}, target.getLocalPosition());

                let cameraPos,cameraRot,targetPos;

                for(let i=0; i<target.children.length; i++) {
                    if(target.children[i].name != "RootNode"){
                        if(target.children[i].camera){
                            cameraPos = Object.assign({}, target.children[i].getLocalPosition());
                            cameraRot = Object.assign({}, target.children[i].getLocalEulerAngles());
                        }else if(target.children[i].tags._list[0] === "tween"){
                            targetPos = Object.assign({}, target.children[i].getLocalPosition());
                        }
                        // console.log(target.children[i]);
                    }
                }

                // カメラ移動
                t_camera.tween(t_camera.getLocalPosition()).to({
                    x:target.getLocalPosition().x+cameraPos.x,
                    y:target.getLocalPosition().y+cameraPos.y,
                    z:target.getLocalPosition().z+cameraPos.z
                }, 1, pc.SineOut).start();
                t_camera.tween(t_camera.getLocalEulerAngles()).rotate(cameraRot, 1, pc.Linear).start();
                // ビル階層移動
                target.tween(target.getLocalPosition()).to({
                    x:target.getLocalPosition().x+targetPos.x,
                    y:target.getLocalPosition().y+targetPos.y,
                    z:target.getLocalPosition().z+targetPos.z
                }, 1, pc.SineOut).on("complete",function(){
                    target.tags.add("isopen");
                    v_self.clickAnimation = false;
                }).start();

                v_self.DOMhead = "ただいま、" + target.name + "";
                v_self.DOMcontent = "ここは" + target.name + "階です。ここにはその階に応じた説明文を記入する感じになります。";
            },
            oncloseClick: function() {
                const v_self = this;
                v_self.floors[v_self.lastTarget].tags.remove("isopen");
                // カメラ移動
                t_camera.tween(t_camera.getLocalPosition()).to({
                    x:cameraPosOri.x,
                    y:cameraPosOri.y,
                    z:cameraPosOri.z
                }, 1, pc.SineOut).start();
                t_camera.tween(t_camera.getLocalEulerAngles()).rotate(cameraRotOri, 1, pc.SineOut).start();
                v_self.clickAnimation = true;
                // ビル階層移動
                v_self.floors[v_self.lastTarget].tween(v_self.floors[v_self.lastTarget].getLocalPosition()).to(v_self.targetPosOri[v_self.lastTarget], 1, pc.SineOut)
                    .on("complete",function(){ v_self.clickAnimation = false; })
                    .start();
                v_self.openfloor = false;

                v_self.DOMhead = tHead;
                v_self.DOMcontent = tContent;
            },
            oncloseRcClick: function() {
                const v_self = this;
                v_self.DOMraycastFlag = false;
                v_self.DOMraycastHead = "";
                v_self.DOMraycastContent = "";
            }
        }
    });

};

Attributesに設定はこんな感じ
スクリーンショット 2020-02-12 16.30.12.png

私はこういうDOMを操作する系のスクリプトはRootに毎度のごとく登録しています。
特に意味はないですが、なんとなく早く読み込んでほしいと思いと見やすいし管理しやすいからですね。
スクリーンショット 2020-02-12 16.29.50.png

Attributesに登録されているHTMLとCSSのコードも載せておきます。

base.html

Vueの構文が色々書いていますが、階層がOpenかCloseかをv-ifで見ていたり、どの階層をクリックしたかを@clickでイベント取得したりしています。
class="detail is-ray"の要素は、レイキャストでクリックしたスポットの情報を表示するDOMですね。
レイキャストはこの後説明します。

<div id="app" class="wrapper">
    <nav v-if="!rotateFlag" class="select" :class="openfloor ? '' : 'is-open'">
        <div class="select__inner">
            <div class="item" v-for="(floor,index) in floors" @click="onfloorClick(floor,index)"><span>{{floor.name}}</span></div>
        </div>
    </nav>
    <div v-if="!rotateFlag" class="detail" :class="{'is-open':openfloor}">
        <div class="closeBtn" v-if="openfloor&&!DOMraycastFlag" @click="oncloseClick()"></div>
        <div class="detail__inner">
            <p class="domhead" v-html="DOMhead"></p>
            <p class="domcontent" v-html="DOMcontent"></p>
        </div>
    </div>
    <div class="detail is-ray" v-if="DOMraycastFlag">
        <div class="closeBtn" @click="oncloseRcClick()"></div>
        <div class="detail__inner">
            <p class="domhead" v-html="DOMraycastHead"></p>
            <p class="domcontent" v-html="DOMraycastContent"></p>
        </div>
    </div>
    <div class="rotateCheck" v-if="!openfloor"><label for="rotate"><input type="checkbox" id="rotate" v-model="rotateFlag" /><span>カメラ操作切り替え</span></label></div>
</div>

style.css

本来はreset.cssを記述していますが、長くなってしまうので省きました。
自前のreset.cssを使用していましたが、多分普段使用しているものでも問題ないと思います。

あと、これはモックなのでテキトーなスタイルなのであまり参考にならないかもしれません。

body {
    background-color: #b1b1b1;
}

.wrapper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 10;
}
.select {
    position: absolute;
    top: 0;
    right: 0;
    z-index: 5;
    transform: translateX(400px);
    transition: transform .4s;
}
.select.is-open {
    transform: translateX(0);
}
.select__inner {
    position: absolute;
    top: 0;
    right: 0;
    width: 330px;
    padding: 5px;
    border-radius: 10px;
    background-color: rgba(255,255,255, .5);
}
.item {
    display: inline-block;
    vertical-align: middle;
    margin: 10px;
    padding: 10px;
    border: 1px solid #cccccc;
    border-radius: 50%;
    background-color: #cccccc;
    text-align: center;
    transition: background .3s;
    cursor: pointer;
}
.item span {
    color: #eeeeee;
    font-size: 2rem;
    line-height: 1;
    transition: color .3s;
}
.item:hover {
    background-color: #eeeeee;
}
.item:hover span {
    color: #333333;
}
.closeBtn {
    position: absolute;
    top: -10px;
    right: 0;
    width: 50px;
    height: 50px;
    background-color: #333333;
    transition: background .3s;
    cursor: pointer;
}
.closeBtn:before,.closeBtn:after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 30px;
    height: 2px;
    border-radius: 25%;
    background-color: #eeeeee;
    transition: background .3s;
}
.closeBtn:before {
    transform: translate(-50%,-50%) rotate(45deg);
}
.closeBtn:after {
    transform: translate(-50%,-50%) rotate(135deg);
}
.closeBtn:hover {
    background-color: #eeeeee;
}
.closeBtn:hover:before,.closeBtn:hover:after {
    background-color: #333333;
}
.detail {
    position: absolute;
    bottom: 50%;
    left: 50%;
    z-index: 2;
    transform: translate(-50%,50%);
    max-width: 800px;
    min-width: 420px;
    width: 100%;
    padding: 50px 20px;
    transition: all .4s;
    pointer-events: none;
}
.detail.is-ray {
    pointer-events: auto;
}
.detail * {
    transition: all .6s;
}
.detail__inner {
    padding: 100px 30px;
    border-radius: 10%;
    background-color: rgba(35,35,35, .7);
    text-align: center;
}
.domhead {
    margin-bottom: .5em;
    color: #eeeeee;
    margin-bottom: 10px;
    font-size: 2rem;
    font-weight: bold;
    line-height: 2;
}
.domcontent {
    color: #eeeeee;
    font-size: 1.6rem;
    line-height: 2;
}
.rotateCheck {
    position: fixed;
    bottom: 0;
    right: 0;
    z-index: 10;
    pointer-events: auto;
}
.rotateCheck label {
    display: block;
    cursor: pointer;
}
.rotateCheck label input {
    display: none;
}
.rotateCheck label span {
    display: inline-block;
    padding: 1rem;
    background-color: #cccccc;
    font-size: 1rem;
    line-height: 1;
    transition: color .2s, background .2s;
}
.rotateCheck label input:checked+span {
    color: #eeeeee;
    background-color: #333333;
    pointer-events: none;
}

.detail.is-open {
    bottom: 0;
    left: 0;
    transform: translate(0,0);
    max-width: 100%;
    min-width: 0;
    width: 100%;
    padding: 20px;
    pointer-events: auto;
}
.detail.is-open .detail__inner {
    padding: 20px;
    border-radius: 0;
    background-color: rgba(255,255,255, .7);
    text-align: left;
}
.detail.is-open .domhead {
    color: #111111;
    font-size: 1.2rem;
}
.detail.is-open .domcontent {
    color: #111111;
    font-size: 1rem;
}

レイキャスト

各階層のスポットを紹介するためにレイキャストでクリックできるポインターを設置します。
各階層の子としてEntityを追加します。
配置は真上から見て良い感じの場所に配置させておきましょう。
スクリーンショット 2020-02-12 17.14.55.png

domRaycast.js

ポインターを追加できたらスクリプトを作ります。
今回のレイキャストはDOMを使ったものになっています。
そのためstyleのコードばかりですが、updateで書いているコードがレイキャストの処理になります。

DOMでのレイキャスト以外にも方法ありますので、別記事を参照ください

/*jshint esversion: 6, asi: true, laxbreak: true*/
const DomRaycast = pc.createScript('domRaycast');

DomRaycast.attributes.add("cameraEntity", {type: "entity", title: "Camera Entity"});
DomRaycast.attributes.add("a_domhead", {type: "string", title: "DOM Head"});
DomRaycast.attributes.add("a_domcontent", {type: "string", title: "DOM Content"});

// g_domRC = {};
// g_domRC.flag = false;
// g_domRC.domhead = "test";
// g_domRC.domcontent = "testest";

DomRaycast.prototype.initialize = function() { // init
    let self = this;
    self.directionToCamera = new pc.Vec3();
    self.defaultForwardDirection = self.entity.forward.clone();

    self.btn = document.createElement("div");
    document.getElementsByTagName("canvas")[0].parentNode.appendChild(self.btn);
    self.btn.style.position = "absolute";
    self.btn.style.width = "30px";
    self.btn.style.height = "30px";
    self.btn.style.borderRadius = "50%";
    self.btn.style.background = "#111111";
    self.btn.style.transition = "opacity .5s";
    self.btn.style.zIndex = 10;
    self.btn.style.cursor = "pointer";
    self.btn.style.pointerEvents = "none";
    self.btn.addEventListener("mouseover",function(){ this.style.background = "#555555"; });
    self.btn.addEventListener("mouseout",function(){ this.style.background = "#111111"; });
    self.btn.addEventListener("mousedown",function(){ this.style.background = "#aaaaaa"; });
    self.btn.addEventListener("mouseup",function(){
        vueApp.DOMraycastFlag = true;
        vueApp.DOMraycastHead = self.a_domhead;
        vueApp.DOMraycastContent = self.a_domcontent;
        this.style.background = "#111111";
    });
};

DomRaycast.prototype.update = function(dt) { // update
    let worldPos = this.entity.getPosition();
    let screenPos = new pc.Vec3();

    this.cameraEntity.camera.worldToScreen(worldPos, screenPos);

    this.directionToCamera.sub2(this.cameraEntity.getPosition(), this.entity.getPosition());
    this.directionToCamera.normalize();
    // let dot = this.directionToCamera.dot(this.defaultForwardDirection);
    if (this.entity.parent.tags._list[0] === "isopen" && !vueApp.DOMraycastFlag) {
        this.btn.style.pointerEvents = "auto";
        this.btn.style.opacity = 1;
    } else {
        this.btn.style.pointerEvents = "none";
        this.btn.style.opacity = 0;
    }

    this.btn.style.transform = "translate(" + screenPos.x + "px," + screenPos.y + "px)";
};

AttributesはCameraとモーダルで表示させるテキスト情報を登録できます。
スクリプトを一つ作って各階層に登録していけるのがPlayCanvasの良いところですよね。

スクリーンショット 2020-02-12 17.16.33.png

背景透過

ここまでで処理は完成していますが、ついでにPlayCanvasのコンテンツ背景を透過させようと思います。
Cameraの設定のClear ColorのAlphaを0に。
スクリーンショット 2020-02-12 17.23.40.png
SETTINGSのRENDERINGの設定内にあるTransparent Canvasをtrueにします。

スクリーンショット 2020-02-12 17.23.56.png

PlayCanvasでのcanvasの透過方法ですが、これも別記事で説明しています。
詳しくはそちらでご参照ください

完成

ここまで設定できたら完成です!
Vue.jsを使うことでデータの受け渡しが楽になるので良いですね。
Vue.jsの良いところはガチガチのフレームワークではなくライブラリ感覚で使えるところでしょうか。
今回はPlayCanvasの中でVue.jsを使ってみましたが、他のjsライブラリでも試して見るもの良いですね。
スクリーンショット 2020-02-12 17.29.06.png

処理のフロー

コードについてあまり説明をできていませんが、流れについて説明しておきましょう。
画像は簡単なものなのでわかりづらいですが石とか投げないでください。
名称未設定-2.png

今回の大元はchangeFloor.jsですが、基本的にはopenする階層を選択したら移動するというのが主な仕事です。
これを行うためには、各それぞれの要素がどんな動きをしているのか、どんな状態で待機しているのかを管理する必要がありました。
今回で言えば、autoRotate.jsは自動回転を始めてしまいますから、階層をopenにした後にも自動回転してしまっては思った挙動にならないです。
DOMについても表示すべき場面で表示させないといけません。
そのため、changeFloor.jsは名前的には階層を変える処理を行うのですが、他の処理をコントロールする中枢の処理を担っています。

反省

なるべく管理のしやすい、見やすいを心がけて作りましたが、コードはあんまり綺麗にできませんでした…
ゲーム開発はやったことないのでどうするのが一番良いのかわからないですが、なるべく誰でも見た感じでわかるようなプロジェクトを作っていきたいですね。

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

【JavaScript】フォーム上などの画像ファイルをブラウザだけでリサイズ

JavaScriptで画像ファイルをリサイズしたい時があると思います。僕はありました。
単にFileを投入して変換するコードが意外となかったのでここに共有します。
この例では、リサイズ後の画像をブラウザからダウンロードの形で取得できるようにしました。

DEMO

See the Pen GRJRrPW by hiroism (@jukaism) on CodePen.

Canvasを利用します

Canvasは図を表示するための仕様ですが、機能が豊富なので画像加工にも利用できます。

  <body>
    <canvas style="width: 484px; height: 253px;" id="canvas"></canvas>
  </body>

画像をリサイズしたい幅と高さを指定しておきます。邪魔ならdisplay: none;も可。

コード全体

htmlファイルにしてブラウザで開けば動くコードです。
スクリプト内のresize(file)にFileを投入するほか、
添付フィールドを用意したので、ここから画像を選んでも動作します。
image.png

<html lang="ja">
  <body>
    <input type="file" id="image_zone">
    <br>
    <canvas style="width: 484px; height: 253px;" id="canvas"></canvas>
    <script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script> <!-- IEで必要。不要なら削除を -->
    <script>
      // image_zoneに新しいファイルがセットされた時、resizeを呼ぶ
      const imageZone = document.getElementById('image_zone')
      imageZone.addEventListener('change', resizePinnedImage, false)
      function resizePinnedImage(e) {
          const file = e.target.files[0]
          if (!file.type.match('image.*')) { return }
          resize(file)
      }
      // Canvasに画像をリサイズして貼り、終わったらブラウザからダウンロードっぽく取得
      function resize(file) {
        imageToCanvas(file).then(function (canvas) {
          // [5] キャンバスから画像を取得
          if (canvas.msToBlob) {
            window.navigator.msSaveBlob(canvas.msToBlob(), 'resized.png')
          } else {
            const a = document.createElement('a')
            a.href = canvas.toDataURL(file.type)
            a.download = 'resized.' + file.type.split('/')[1]
            a.click()
          }
        }).catch(function (error) {
          console.error(error)
        })
      }

      function imageToCanvas (imageFile) {
        // [1] Fileから画像データを得る
        return new Promise(function (resolve, reject) {
          readImage(imageFile).then(function (src) {
            loadImage(src).then(function (image) {
              // [2] Canvasの呼び出し
              const canvas = document.getElementById("canvas")
              const ctx = canvas.getContext('2d')
              // [3] 縮尺を計算
              const scale = Math.max((canvas.height / image.height), (canvas.width / image.width))
              // [4] 画像を切り取り、Canvasに展開して返す
              ctx.drawImage(
                // Canvasに貼り付ける画像
                image, 
                // 画像の切り取り開始位置(左上からの横距離、縦距離)
                (image.width - (canvas.width / scale)) / 2, (image.height - (canvas.height / scale)) / 2,
                // 画像の切り取り範囲(幅、高さ)
                canvas.width / scale, canvas.height / scale,
                // キャンバスの貼り付け開始位置(左上からの横距離、縦距離)
                0, 0,
                // 画像への貼り付け範囲(幅、高さ)
                canvas.width, canvas.height
              )
              resolve(canvas)
            }).catch(function (error) {
              reject(error)
            })
          }).catch(function (error) {
            reject(error)
          })
        })
      }

      // [1-A] Fileの中身を得る
      function readImage(image) {
        return new Promise(function (resolve, reject) {
          const reader = new FileReader()
          reader.onload = function () { resolve(reader.result) }
          reader.onerror = function (e) { reject(e) }
          reader.readAsDataURL(image)
        })
      }
      // [1-B] 空の画像データを作った後に中身を投入
      function loadImage(src) {
        return new Promise(function (resolve, reject) {
          const img = new Image()
          img.onload = function () { resolve(img) }
          img.onerror = function (e) { reject(e) }
          img.src = src
        })
      }
    </script>
  </body>
</html>

[1] FileをImageにする
FileReaderでFileの中身を取り出し、空のImageに投入します。
このあたりでPromiseを多用しているためIE用のPolyfillを入れています。

    <script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
      // ..略
      function imageToCanvas (imageFile) {
        // [1] Fileから画像データを得る
        return new Promise(function (resolve, reject) {
          readImage(imageFile).then(function (src) {
            loadImage(src).then(function (image) {
            // ここに画像データ(image)が渡ってくるので処理
            }).catch(function (error) {
              reject(error)
            })
          }).catch(function (error) {
            reject(error)
          })
        })
      }
      // [1-A] Fileの中身を得る
      function readImage(image) {
        return new Promise(function (resolve, reject) {
          const reader = new FileReader()
          reader.onload = function () { resolve(reader.result) }
          reader.onerror = function (e) { reject(e) }
          reader.readAsDataURL(image)
        })
      }
      // [1-B] 空の画像データを作った後に中身を投入
      function loadImage(src) {
        return new Promise(function (resolve, reject) {
          const img = new Image()
          img.onload = function () { resolve(img) }
          img.onerror = function (e) { reject(e) }
          img.src = src
        })
      }

[2] HTML上のCanvasの呼び出し
キャンバスと、その描画用のコンテキストを呼び出します。

              // [2] Canvasの呼び出し
              const canvas = document.getElementById("canvas")
              const ctx = canvas.getContext('2d')

[3] 縮尺の計算
アスペクト比を維持したいので、縦横どちらか大きい方の縮小率を採用します。

              // [3] 縮尺を計算
              const scale = Math.max((canvas.height / image.height), (canvas.width / image.width))

[4] 画像を切り取り、Canvasに展開
CanvasのメソッドdrawImageを使って画像を貼り付け。
この時画像の切り取り範囲とCanvasへの貼付け範囲を一気に指定する。混乱しがち。
本当はここで画像にしたかったが、ブラウザによってこの後のデータの取り出し方が違うのでここではCanvasを返す。

              // [4] 画像を切り取り、Canvasに展開して返す
              ctx.drawImage(
                // Canvasに貼り付ける画像
                image, 
                // 画像の切り取り開始位置(左上からの横距離、縦距離)
                (image.width - (canvas.width / scale)) / 2, (image.height - (canvas.height / scale)) / 2,
                // 画像の切り取り範囲(幅、高さ)
                canvas.width / scale, canvas.height / scale,
                // キャンバスの貼り付け開始位置(左上からの横距離、縦距離)
                0, 0,
                // 画像への貼り付け範囲(幅、高さ)
                canvas.width, canvas.height
              )
              resolve(canvas)

[5] Canvasから画像を取得
Canvasにリサイズ後の画像が表示され、Canvasの各種メソッドでDataURLやBlobが取得出来る。
サンプルではブラウザからダウンロードできるようにした。IEでもいけますがpng固定になるので注意。

          // [5] キャンバスから画像を取得
          if (canvas.msToBlob) { // IEの場合
            window.navigator.msSaveBlob(canvas.msToBlob(), 'resized.png')
          } else { // その他ブラウザ
            const a = document.createElement('a')
            a.href = canvas.toDataURL(file.type)
            a.download = 'resized.' + file.type.split('/')[1]
            a.click()
          }

以上です。

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

Wordpressの管理画面にオリジナルのCSS・JavaScriptを適用させる

フォルダ構成

qiita.png

必要な記述

functions.php
<?php
/**
 * エディタースタイル適用
 * */

    add_editor_style(get_template_directory_uri()."/css/admin/editor-style.css");

/**
 * 管理画面スタイル適用
 * */

function my_admin_stylesheet()
{
    wp_enqueue_style('custom-admin-style', get_stylesheet_directory_uri().'/css/admin/admin-style.css');
    wp_enqueue_script('admin-script', get_stylesheet_directory_uri() . '/js/admin/admin-style.js', array(), '1.0.0', true);
}
add_action('admin_enqueue_scripts', 'my_admin_stylesheet');

/**
 * 管理画面にもfaviconをつける
 * */
function admin_favicon()
{
    echo '<link rel="shortcut icon" type="image/x-icon" href="'.get_stylesheet_directory_uri().'/img/favicon.ico" />';
}
add_action('admin_head', 'admin_favicon');

/**
 * ログイン画面CSS変更
* */

function my_login_stylesheet()
{
    wp_enqueue_style('custom-login', get_stylesheet_directory_uri().'/css/admin/login-style.css');
}
add_action('login_enqueue_scripts', 'my_login_stylesheet');
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PlayCanvasの背景を透過canvasにする方法

canvasの背景透過

HTML5のcanvas要素は毎フレームでレンダリングを行います。
canvasの背景を透過したいなんて声はググったりすると意外と多い気がします。

ちなみに、canvasの背景を透過したいなんて時は以下のようにalpha値を指定してあげれば良いです。

gl.clearColor(0, 0, 0, 0)

PlayCanvasの背景透過

PlayCanvasはどうしたらいいか。

PlayCanvasエディターのRENDERINGにTransparent Canvasという項目があります。
これにチェックを入れます。

スクリーンショット 2019-12-26 17.37.08.png

これだけで背景は透明になったかというとそうでもなかったり…

EntityにCameraがある場合は、このCameraのClear ColorのAlpha値を0にしてあげるとちゃんと透過されます。

スクリーンショット 2019-12-26 17.39.30.png

また、SkyBoxを入れている場合もちゃんと透過されないのでご注意を。

これでCSSからbackground-colorを変更してあげると透過されているのがわかります。
スクリーンショット 2019-12-26 17.42.31.png
スクリーンショット 2019-12-26 17.42.25.png

Webで使う際には透過はよく使うので覚えておきたいですね。

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

HTML+JavaScript の keydown で使える特殊キーの件

概要

とある web アプリを書いていまして。F1 ~ F12 のどれかのキーで「書類の差替」機能を割り付けるという話がありました。
でまあレガシーな web アプリですので、IE 11 / Edge (Project Spartan) / FireFox / Chrome での対応が必要になり云々。

現状調査から手を付ける事にしました。

charCode とブラウザーの機能との対応表

キー e.charCode Chrome 79 IE 11 FireFox 72
F1 112 ヘルプ ヘルプ★ -
F2 113 - - -
F3 114 - ページ内検索 ページ内検索
F4 115 - URL ドロップダウン -
F5 116 ページ更新 ページ更新 ページ更新
F6 117 URL 欄等フォーカス URL 欄フォーカス URL 欄フォーカス
F7 118 - カーソルブラウズ キャレットブラウズモード★
F8 119 - - -
F9 120 - - -
F10 121 メニュー プルダウンメニュー プルダウンメニュー
F11 122 Kiosk Kiosk Kiosk
F12 123 開発者コンソール 開発者コンソール 開発者コンソール

キャンセル可能性

return false; で既定の動作をキャンセル可能かどうか。

上の表で を付けたキーの機能はキャンセルできませんでした。

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

Vue.jsでドロワーメニュー(スライドメニュー)を実装

codesandboxに書いてみたので、動きはこれを見ていただけると!(App.vue参照)
https://codesandbox.io/embed/objective-flower-kuov7?fontsize=14&hidenavigation=1&theme=dark

以下、ざっくり内容をざっくりメモ程度に。

template
<template>
  <div id="app">
    <div>
      <button @click="openDrawerMenu">ボタン</button>
    </div>
    <transition name="right">
      <div v-if="drawerFlg" class="drawer-menu-wrapper">
        <div class="drawer-menu">
          <!-- ここにメニューの内容を書いていく -->
        </div>
      </div>
    </transition>
  </div>
</template>
script
<script>
export default {
  name: "App",
  data() {
    return {
      drawerFlg: false
    };
  },
  methods: {
    openDrawerMenu() {
      this.drawerFlg = true;
    }
  }
};
</script>
style
<style>
//右から出したい場合
.right-enter-active, .right-leave-active {
  transform: translate(0px, 0px);
  transition: transform 225ms cubic-bezier(0, 0, 0.2, 1) 0ms;
}
.right-enter, .right-leave-to {
  transform: translateX(100vw) translateX(0px);
}

//左から出したい場合
.left-enter-active, .left-leave-active {
  transform: translate(0px, 0px);
  transition: transform 225ms cubic-bezier(0, 0, 0.2, 1) 0ms;
}
.left-enter, .left-leave-to {
  transform: translateX(-100vw) translateX(0px);
}

//以下、メニューの形に合わせて良い具合に変更してください
.drawer-menu-wrapper {
  position: absolute;
  z-index: 10;
  top: 0;
  right: 0; //右に出す場合
  left: 0 //左に出す場合
  width: 50%;
  height: 100%;
  background-color: white;
}
.drawer-menu {
  padding: 24px;
}
</style>

参考: https://qiita.com/Nexus0831/items/9dbbac367a653fbd8ba4

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

expo-cliというかnpmでconfigure errorが出たときの対応

いつも使ってるexpo-cliを利用しようとしたら、下記のようなエラーに遭遇した。

gyp WARN EACCES current user ("nobody") does not have permission to access the dev dir "/Users/xxxxxx/Library/Caches/node-gyp/12.3.1"
gyp WARN EACCES attempting to reinstall using temporary dev dir "/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/expo-cli/node_modules/chokidar/node_modules/fsevents/.node-gyp"
gyp WARN install got an error, rolling back install
gyp WARN install got an error, rolling back install
gyp ERR! configure error
gyp ERR! stack Error: EACCES: permission denied, mkdir '/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/expo-cli/node_modules/chokidar/node_modules/fsevents/.node-gyp'
gyp ERR! System Darwin 19.3.0
gyp ERR! command "/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/bin/node" "/Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js" "rebuild"
gyp ERR! cwd /Users/xxxxxx/.anyenv/envs/nodenv/versions/12.3.1/lib/node_modules/expo-cli/node_modules/chokidar/node_modules/fsevents
gyp ERR! node -v v12.3.1
gyp ERR! node-gyp -v v5.0.5
gyp ERR! not ok

結論から言えば、下記のように--unsafe-permオプションをつけて実行すればインストール自体はできた。

sudo npm install --unsafe-perm -g expo-cli

基本的にroot権限でnpm installすることは推奨されていないようで、場合により--unsafe-permオプションが必要なようです。

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

Cloud Firestore まとめ

概要

  • サーバーレスなKeyValueのデータストア
  • Cloud functionsから利用できる
  • クライアント側のアプリからも直接アクセスできる
    • セキュリティ設定に注意が必要
  • データの更新について、リアルタイムに通知が受け取れる

簡単なサンプル

CloudFunctionsからデータを取得する

import * as admin from 'firebase-admin';

const userId = 'test_user'

const user = (await admin
  .firestore()
  .collection('users')
  .doc(userId)
  .get()).data();

console.info(JSON.stringify(user));
  • firebase-adminを使用すると、Firestoreのアクセス制限の設定を無視してデータを取得できる
    • CloudFunctionsから利用するときはfirebase-adminを利用して、別途CloudFunctions側でアクセス制限する
  • CloudFunctionsからではなくクライアント側から直接Firestoreにアクセスするような時は、Firestoreのアクセス制限機能を利用する

気にするべき特徴

  • 1秒に1回しか更新できない
  • インデックスがないと検索できない
    • whereに入れるためにはインデックスが必要
    • なければ自動で作るためのURLをログに出してくれるので、そんなに面倒ではない
  • !=で検索できない
    • 国籍が日本以外のデータを検索、みたいなことができない
      • 他の国籍全てについて==で検索して、全ての結果をマージするしかない
  • 範囲検索が一つしかできない
    • 年齢xx歳以上、体重xx以上、のように複数の範囲条件が指定できない
      • 別々に検索してマージするしかない
    • orderByが範囲検索と捉えられるため、実質は一個しか指定できない感じ
    • よくある「商品データベース」みたいなものに利用すると、検索できなくなって困ると思う
  • トランザクションがある
    • 既に他の誰かが編集中だったりすると失敗してリトライされる
  • 検索条件などに癖があるため、利用はきちんと検討してからにするべき

アクセス制限について

  • rulesファイルを書く
  • アクセス制限のことを考えてデータ構造を設計しなくてはならない

データの項目にユーザーIDを入れておいてアクセス制限する例

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{uid} {
      allow list: if request.auth != null && (request.auth.uid == resource.data.uid)
    }
  }
}

グループに所属するユーザーのみにアクセスを許可する例

  • グループに所属しているかどうかを調べるのにwhere的に検索したいが、そのようなことはできない
  • 仕方ないのでコレクションキーuidにして、existsで調べる
service cloud.firestore {
  match /databases/{database}/documents {
    match /groups/{gid} {
      allow read: if request.auth != null && exists(/databases/$(database)/documents/groups/$(gid)/members/$(request.auth.uid));
    }
  }
}

参考サイト

目次

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

Cloud Functions まとめ

概要

  • アプリケーションのサーバーサイドのプログラムを実行する
  • 主にAPIを作成するために利用される
  • 関数一つが一つのAPIになるイメージ
  • Authenticationサービスと連携することによってアクセス制限が可能

簡単なサンプル

  • 実行すると、CloudFunctionsのログ(管理コンソールから確認できる)にHello sfjwr !!と表示される

CloudFunctions側のAPI

import * as functions from 'firebase-functions';

export const hello = functions.https.onCall(
  (params, context) => {
    console.info(`Hello ${params.name} !!`);
  },
);

APIを呼び出すクライアント側

import firebase from 'firebase';

firebase.functions().httpsCallable('hello')({name: 'sfjwr'});

気にするべき特徴

  • Cloud Functions内へのデータの保存はできない

    • 何かを保存したければ、Cloud FirestoreCloud Storageやその他サービスへ渡すしかない
  • 実行時間に制限がある

    • 最大9分
    • そのためバッチ処理には不向き
  • ユーザー側からの呼び出し以外に、Firebaseの他サービスからの通知によって実行することもできる

    • ファイルがCloud Storageにアップロードされた時
      • 自動でサムネイルを作るとか
    • Authenticationでユーザーが作られた時
      • 自サービス用のユーザーデータをFirestoreに放り込むとか
    • 定期実行も可能
      • 1時間毎とか1日毎とか
  • 関数毎にメモリの割り当てを指定することができる

    • 環境変数なども指定できる
    • Firebaseのコンソールからではなく、Google Cloud Platformのコンソールから指定する
    • FirebaseといいつつGoogle Cloud PlatformCloud Functionsと同じなので、Google Cloud Platform側の画面で色々できる
      • StackDriverでログを制御する、とか
  • 結構遅い

    • レスポンスまでに1秒くらいは見ておいた方がいいかも?
    • 特に内部的にインスタンスが立ち上がる時が遅い
      • しばらくアクセスがなくて久しぶりにアクセスが会った時とか
      • 5秒くらいかかるイメージ
    • Firestoreからデータを取得する時にループを回して、中でawaitとかすると凄まじく遅くなる
      • ループではなく並列取得するべき

参考サイト

目次

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

Google Firebase まとめ

GoogleのクラウドサービスであるFirebaseについて、まとめていきます。

はじめに

中級者が気軽にパッと見てある程度の内容が把握できる、くらいのところを目標に記述していきます。初心者向けではないです。
なるべくわかりやすくするため、基本的に短文箇条書き形式にて記します。
サンプルコードが必要なところはJavaScriptにて記載します。

間違いの指摘などあれば、コメントお待ちしております。

Firebaseとは?

  • アプリケーションのサーバーサイドを楽に作れる仕組み
  • 基本的にサーバーレス
  • 例えば以下のような物がサーバーレスで利用できる
    • データベース(Firestore)
    • ファイルサーバー(Storage)
    • API(Cloud functions)
    • Webサーバー(Hosting)

サーバーレスとは?

  • その名の通り、サーバーという概念が無い

    • 実際には裏には存在しているが、意識しなくてよいようになっている
    • イメージ的には、ファイルやアプリケーションのプログラムをアップローダーからアップロードするだけで動くようになるよ、的な感じ
  • メリット

    • メンテナンスの手間が少ない
      • セキュリティパッチやディスクの故障など何も心配する必要がない
      • アクセスが急激に増えても自動スケールアウトで勝手にうまく処理してくれる
  • デメリット

    • 従量制なのでかかる費用がよくわからない
      • 基本的にはオンプレよりは安くなるはず?
    • あまり細かい設定はできない
      • 特殊な要件だと対応できないかも
      • パフォーマンスチューニングとか
        • 一台ごとの性能云々はあまり調整できないが、自動スケールアウトでお金で解決
        • レイテンシが問題になることはあるかも?

主要サービスの概要

Cloud Functions

  • アプリケーションを実行するためのもの
  • 主にAPIを作るために利用できる
  • 言語はJavaScriptPythonGo
  • 手元でコードを書いて、アップロードコマンドを実行することで、CloudFunctionsに配置できる
    • 指定のURLを開く等で実行できる

Cloud Firestore

  • KeyValueのデータベース
  • Cloud Functionsから簡単に利用できる
  • データの更新をリアルタイムに取得できる

Authentication

  • 自作サービスのユーザー登録機能を自分で作らなくてよくするためのもの
    • SMSやE-mailによる本人確認
    • SNSによる本人確認
  • Firebaseのその他の機能と連携できる
    • Authenticationのユーザー情報を利用してCloud Functionsの呼び出しを制限する
    • Authenticationのユーザー情報を利用してFirestoreCloud storageのデータへのアクセスを制限する

Cloud Storage

  • いわゆるファイルサーバー
    • サーバーレスなので勝手にスケールする
    • 同時接続数とか考えなくても良い
  • CloudFunctionsからアップロードされたファイルを保存したり等に利用できる

Hosting

  • いわゆるWebサーバー
    • サーバーレスなので勝手にスケールする
    • 同時接続数とか考えなくても良い
  • 静的なファイルをホスティングできる

Webでよくある開発パターン

  • CloudFunctionsでサーバーサイドのAPIを作る
    • CloudFunctionsからFirestoreへデータベースのデータを格納する
    • ファイルはCloudStorage
  • クライアントサイドをSPAで作る
    • 作ったSPAHostingでWebに配置
    • SPAからはCloudFunctionsAPIを叩きに行く

各サービス毎のまとめ

以下にまとめて行きます。

参考サイト

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

TypeScript + React + Sass + Babelを利用したWebpack環境構築

TypeScript + React + Sass + Babel利用したWebpack環境構築

最近, TypeScriptを勉強し始めたので
今回はTypeScript + React + Sass + Babelを利用した
Webpackの環境構築を行っていきたいと思います。

前提

  • ターミナルが利用できる
  • Node.jsを利用環境がある
  • npm, yarnの利用環境がある

環境

  • 2020/2/12 時点での最新モジュール
  • Node v11.10.1
  • ES6+
  • TypeScript v3.7.4
  • React.js v16.12.0
  • Webpack v4.41.2

ファイル構成

ファイル構成は以下の通りです。
スクリーンショット 2020-02-12 10.39.16.png

前準備

以下のpackage.jsonを作成し,各npmモジュールを
npm install もしくは, yarn add package.json
を利用してインストールしてください。

package.json
{
  "name": "",
  "version": "1.0.0",
  "description": "",
  "main": "Main.tsx",
  "scripts": {
    "build": "webpack",
    "start": "webpack-dev-server"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0",
    "@babel/core": "^7.7.4",
    "@babel/preset-env": "^7.7.4",
    "@babel/preset-react": "^7.7.4",
    "@types/react": "^16.9.17",
    "@types/react-dom": "^16.9.4",
    "@types/react-router-dom": "^5.1.3",
    "autoprefixer": "^9.7.4",
    "babel-loader": "^8.0.6",
    "babel-minify-webpack-plugin": "^0.3.1",
    "core-js": "3",
    "css-loader": "^3.2.0",
    "html-webpack-plugin": "^3.2.0",
    "mini-css-extract-plugin": "^0.9.0",
    "node-sass": "^4.13.0",
    "package.json": "^2.0.1",
    "postcss-loader": "^3.0.0",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-router-dom": "^5.1.2",
    "sass-loader": "^8.0.0",
    "ts-loader": "^6.2.1",
    "typescript": "^3.7.4"
  },
  "devDependencies": {
  }
}

これで環境構築に必要なモジュールが全てnode_modules配下に
インストールされました。

今回はメイン環境構築ですので, ささっとしたい方については
srcディレクトリ配下のファイルMain.tsxについて,
こちらを参照して各ディレクトリ上に作成してください。
https://github.com/olt556/react_hooks_ts_tmp

Webpackの設定

次にWebpackの設定ファイルとなる,
webpack.config.jsを作成していきます。

webpack.config.js
const path = require('path');
// Babelの機能のminifyを利用するため
const BabelMinifyPlugin = require("babel-minify-webpack-plugin");
// ビルドする際にHTMLも同時に出力するため
const HtmlWebpackPlugin = require('html-webpack-plugin');
// CSSをJSにバンドルせずに出力するため
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    mode: 'development',
    // pathの設定についてですがpathモジュールを使う必要は特にはありません。
    entry: path.resolve(__dirname, 'src/Main.tsx'),
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    devServer: {
        contentBase: path.resolve(__dirname, 'dist'),
        port: 8080,
        historyApiFallback: true, // これがないとルーティングできない
    },
    resolve: {
        modules: [path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules')],
        extensions: ['.ts', '.tsx', '.js']
    },
    module: {
        rules: [
            // scssのローダ設定
            {
                test: [/\.css$/, /\.scss$/],
                exclude: /node_modules/,
                loader: [MiniCssExtractPlugin.loader, 'css-loader?modules', 'postcss-loader', 'sass-loader'],
            },
            // js,ts,tsxのローダ設定
            {
                test: [/\.ts$/, /\.tsx$/, /\.js$/],
                loader: ['babel-loader', 'ts-loader'],
            },
        ],
    },
    plugins: [
        new BabelMinifyPlugin(),
        new HtmlWebpackPlugin({
            publicPath: 'dist', // ビルド後のHTMLの出力先
            filename: 'index.html', //出力するHTMLのファイル名
            template: 'src/html/index.html', //出力するためのHTMLのテンプレート
        }),
        new MiniCssExtractPlugin({
            publicPath: 'dist', // ビルド後のCSSの出力先
            filename: 'app.css', //出力するCSSのファイル名
        }),
    ],
}

webpack.config.jsは以上のようになります。
ちなみに, ローダの設定についてですが,
loader: ['babel-loader', 'ts-loader']
の場合, ts-loaderbabel-loaderの順に読み込まれます。

Babelの設定

Babelの設定は .babelrc に記述していきます。

{
    "presets": [
        ["@babel/preset-env", { //babelの設定
            "useBuiltIns": "usage",
            "corejs": 3 // polyfill用の設定
        }],
        "@babel/preset-react", // react用のbabelの設定
        ["minify",{}] // minifyの設定
    ]
}

TypeScriptの設定

続いてTypeScriptのトランスパイル(コンパイル)設定をtsconfig.json
記述してきます。

tsconfig.json
{
    "compilerOptions": {
        "sourceMap": true,
        "noImplicitAny": true,
        "allowJs": true,
        "strictNullChecks": true,
        "module": "ES6",
        "target": "es5",
        "jsx": "react"
    },
    "include": [
        "src"
    ],
    "exclude": [
        "node_modules"
    ],
}

各々の compilerOptions のオプションについては,
こちらを参照するといいかもしれません。
https://qiita.com/ryokkkke/items/390647a7c26933940470

AutopreFixerの設定

AutopreFixer はPostCSSの機能ですので,
利用できるようPostCSSの設定をpostcss.config.jsに記述していきます。

postcss.config.js
module.exports = {
    plugins: [
        require("autoprefixer")({
            grid: "autoplace",
            browsersList: ["ie >= 11"]
        })
    ],
};

ビルドについて

以上の設定ファイルを作成したのち,
npm run build を実行することで,
エントリーポイントであるMain.tsxsrc配下のファイル
読み込んでビルドを行い, dist以下に出力することができます。

また, npm run start でリアルタイムでビルドを行い,
出力されたファイルが読み込まれた, ローカルサーバが
htttp://localhost:8080 で起動します。

おわりに

間違えや質問などありましたら,
お気軽にコメントしていただけると幸いです。
今回はTypeScriptのテンプレートについては端折っているため,
そちらの記述方法などは,
GitHubを参照していただけたらと思います。

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

【JavaScript】querySelectorなどでCSSセレクターを使うときに](ブラケット)を閉じないとSafariで動いてくれない

次のように、querySelectorで要素を取得する際にうっかり]を書き忘れた場合でも、Chromeでは普通に動いてくれます。

foo.html
<input type="text" value="hogehoge" />

<script>
    const element = document.querySelector("input[type='text'")
    console.log(element.value)
</script>

Chromeの場合

スクリーンショット 2020-02-12 9.27.43.png

しかし、Safariで同じことをやろうとすると、次のようなエラーを吐いてしまいます。
The string did not match the expected pattern.

スクリーンショット 2020-02-12 9.25.29.png

スクリーンショット 2020-02-12 9.26.28.png

これはブラケットをしっかり閉じてあげるだけで解決します。

foo.html
<input type="text" value="hogehoge" />

<script>
    const element = document.querySelector("input[type='text']")
    console.log(element.value)
</script>

スクリーンショット 2020-02-12 9.29.59.png

どうやらSafari限定でエラーを吐くようです。

Consider this markup:


And this is where the browser responses to different incomplete selector patterns differ:

document.querySelector('select[name');
Chromium: found 1
Edge: found 1
Firefox: found 1
Internet Explorer: found 1
Safari: DOM Exception 12

引用元 On handling invalid selector strings

普通のブラウザだと正常に動作するのも相まって気が付きにくですね

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

Chart.jsを使用して複数軸のグラフを表示する(Javascript)

はじめに

やりたいこと

<グラフ表示>
・現在日付から過去1カ月のデータを取得する。
・取得したデータをchart.jsにてグラフ表示する。
(X軸:日付、Y軸1-棒グラフ:Qiita-View数、Y軸2-折れ線グラフ:Qiitaいいね・ストックの合計)

<テーブル>
・取得したデータの中で最新日付のデータをテーブルを表示する。

使用技術

  • Javascript
  • Chart.js

デモサイト

デモサイト

※これをみると、投稿がまったく見られないなんてことはなく、毎日少なからずViewがあるんだなぁと思います。(さすがQiita)

1.png
2.png

ポイント

  • グラフの描画部分
sample.js
// グラフを描画する
var ctx = document.getElementById("canvas").getContext("2d");
window.myChart = new Chart(ctx, {
  type: 'bar',
  data: barChartData,            //グラフデータをセット
  options: complexChartOption    //データオプションをセット
});
  • グラフデータのセット
sample.js
// グラフデータのセット
var barChartData = {
  labels: labelData,   //ラベルデータのセット(日付)
  datasets: [
    {
      type: 'line',
      label: 'Total-Likes/Stocks',
      data: lineData,     //lineデータのセット(LIKES+STOCKSの合計)
      borderColor: "rgba(060,179,113,0.8)",
      pointBackgroundColor: "rgba(060,179,113,0.8)",
      fill: false,
      yAxisID: "y-axis-1",
    },
    {
      type: 'bar',
      label: 'Total Views',
      data: barData,       //barデータのセット(VIEWSの合計)
      borderColor: "rgba(54,164,235,0.8)",
      backgroundColor: "rgba(54,164,235,0.5)",
      yAxisID: "y-axis-2",
    },
  ],
};
  • グラフオプションの設定 ticksは取得できた値に応じて、動的に算出する。
sample.js
// グラフオプションの設定
var complexChartOption = {
  responsive: true,
  scales: {
  yAxes: [
    {
      id: "y-axis-1",
      type: "linear",
      position: "left",
      ticks: {
        max: setgoodsMax,     //lineデータのメモリ最大値をセット
        min: setgoodsMin,     //lineデータのメモリ最小値をセット
        stepSize: 10          //lineデータのメモリ幅をセット
      },
    },
    {
      id: "y-axis-2",
      type: "linear",
      position: "right",
      ticks: {
        max: setviewsMax,     //barデータのメモリ最大値をセット
        min: setviewsMin,     //barデータのメモリ最小値をセット
        stepSize: 1000        //barデータのメモリ幅をセット
      },
      gridLines: {
      drawOnChartArea: false,
      },
    }
  ],
};

全体のコード

jsgraph.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Qiita Item Get Graph Display</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
  </head>

  <body onload="getData()">
    <div class="container">
      <br>
      <h3>Qiitaデータグラフ表示</h3>
      <br>
      <div class="container">
        <canvas id="canvas"></canvas>
      </div>
      <div class="container">
        <div id="result"></div>
      </div>
    </div>

    <script src="https://code.jquery.com/jquery-2.1.1.js" integrity="sha256-FA/0OOqu3gRvHOuidXnRbcmAWVcJORhz+pv3TX2+U6w=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.bundle.min.js"></script>

    <script>
      function getData(){

        var qiitadata = [];

        // 現在日付の設定
        var date = new Date();
        var yyyy = date.getFullYear();
        var mm = toDoubleDigits(date.getMonth() + 1);
        var dd = toDoubleDigits(date.getDate());
        var yyyymmdd = yyyy + mm + dd;
        var hyphendate = yyyy + "-" + mm + "-" + dd;

        // 1桁の数字を0埋めで2桁にする
        function toDoubleDigits(num){
          num += "";
          if (num.length === 1) {
            num = "0" + num;
          }
          return num;
        };

        // DB情報の取得
        var request = new XMLHttpRequest();
        request.open('GET', 'https://anotherskyjp.site/php_api/qiita_data_getMonth.php?setdate=' + yyyymmdd, true);
        request.responseType = 'json';

        request.onload = function () {
          qiitadata = this.response;
          drawGraph(qiitadata);                // グラフの描画
          drawTable(qiitadata, hyphendate);    // テーブルの描画
        };

        request.send();

      };

      // グラフの描画
      function drawGraph(qiitadata) {

        var nowdate;
        var sumviews = 0;
        var sumgoods = 0;
        var labelData = [];
        var lineData = [];
        var barData = [];

        // 取得データをループして、グラフ描画データにセットする
        for(var i = 0; i < qiitadata.length; i++){

          // 初回は日付のセット
          if(i == 0){
            nowdate = qiitadata[i].setdate;
          }else{
            // 日付が変わったとき
            if(nowdate != qiitadata[i].setdate){
              //データをセット
              labelData.push(nowdate);
              lineData.push(sumgoods);
              barData.push(sumviews);
              //日付のセットと合計初期化
              nowdate = qiitadata[i].setdate;
              sumgoods = 0;
              sumviews = 0;
            };
          };

          sumgoods = sumgoods + Number(qiitadata[i].likes) + Number(qiitadata[i].stocks);
          sumviews = sumviews + Number(qiitadata[i].views);

          // ループの最後の処理
          if(i == (qiitadata.length - 1)){
            labelData.push(nowdate);
            lineData.push(sumgoods);
            barData.push(sumviews);
          }

        };

        // セットされたデータからticksを算出
        var goodsMax = Math.max.apply(null, lineData);
        var setgoodsMax = goodsMax + 10;
        do {
          setgoodsMax += 1;
        } while ("00" != String(setgoodsMax).substr(-2,2));

        var goodsMin = Math.min.apply(null, lineData);
        var setgoodsMin = goodsMin - 10;
        do {
          setgoodsMin -= 1;
        } while ("00" != String(setgoodsMin).substr(-2,2));

        var viewsMax = Math.max.apply(null, barData);
        var setviewsMax = viewsMax + 1000;
        do {
          setviewsMax += 1;
        } while ("000" != String(setviewsMax).substr(-3,3));

        var viewsMin = Math.min.apply(null, barData);
        var setviewsMin = viewsMin - 1000;
        do {
          setviewsMin -= 1;
        } while ("000" != String(setviewsMin).substr(-3,3));

        // グラフデータのセット
        var barChartData = {
          labels: labelData,
          datasets: [
            {
              type: 'line',
              label: 'Total-Likes/Stocks',
              data: lineData,
              borderColor: "rgba(060,179,113,0.8)",
              pointBackgroundColor: "rgba(060,179,113,0.8)",
              fill: false,
              yAxisID: "y-axis-1",
            },
            {
              type: 'bar',
              label: 'Total Views',
              data: barData,
              borderColor: "rgba(54,164,235,0.8)",
              backgroundColor: "rgba(54,164,235,0.5)",
              yAxisID: "y-axis-2",
            },
          ],
        };

        // グラフオプションの設定
        var complexChartOption = {
          responsive: true,
          scales: {
            yAxes: [
              {
                id: "y-axis-1",
                type: "linear",
                position: "left",
                ticks: {
                  max: setgoodsMax,
                  min: setgoodsMin,
                  stepSize: 10
                },
              },
              {
                id: "y-axis-2",
                type: "linear",
                position: "right",
                ticks: {
                  max: setviewsMax,
                  min: setviewsMin,
                  stepSize: 1000
                },
                gridLines: {
                  drawOnChartArea: false,
                },
              }
            ],
          }
        };

        // グラフを描画する
        var ctx = document.getElementById("canvas").getContext("2d");
        window.myChart = new Chart(ctx, {
          type: 'bar',
          data: barChartData,
          options: complexChartOption
        });

      };

      // テーブルの描画
      function drawTable(qiitadata, hyphendate) {

        // 配列の中の最大値を取得する
        var maxno = Math.max.apply(null, qiitadata.map(function(o){return o.no;}));
        // console.log(maxno);

        var sum_view = 0;
        var sum_like = 0;
        var sum_stock = 0;

        var html = '<br><h3>Qiita記事一覧(' + hyphendate + '時点)</h3><br>' +
                  '<table class="table">' +
                  '<thead class="thead-dark">' +
                  '<tr><th scope="col">No</th><th scope="col">タイトル</th><th scope="col">VIEWS</th><th scope="col">LIKES</th><th scope="col">STOCKS</th><th scope="col">LIKE率</th><th scope="col">STOCK率</th></tr></thead><tbody>'

        // 取得データをループして、グラフ描画データにセットする
        for(var i = 0; i < qiitadata.length; i++){
          // 本日日付になったら、格納処理
          if(qiitadata[i].setdate == hyphendate){

            html +=
              '<tr>' +
              '<th scope="row">' + (maxno + 1 - Number(qiitadata[i].no)) + '</th>' +
              '<td><a href="' + qiitadata[i].url + '" target="_blank">' + qiitadata[i].title + '</a></td>' +
              '<td>' + qiitadata[i].views + '</td>' +
              '<td>' + qiitadata[i].likes + '</td>' +
              '<td>' + qiitadata[i].stocks + '</td>' +
              '<td>' + qiitadata[i].per_like + '</td>' +
              '<td>' + qiitadata[i].per_stock + '</td>' +
              '</tr>';

              sum_view += Number(qiitadata[i].views);
              sum_like += Number(qiitadata[i].likes);
              sum_stock += Number(qiitadata[i].stocks);

          };
        };

        // html に合計を設定し、書き出し
        html +=
          '<tr class="table-warning">' +
          '<th scope="row">計</th>' +
          '<td></td>' +
          '<td>' + sum_view + '</td>' +
          '<td>' + sum_like + '</td>' +
          '<td>' + sum_stock + '</td>' +
          '<td>' + '' + '</td>' +
          '<td>' + '' + '</td>' +
          '</tr>' +
          '</tbody></table>';

        var result = $("#result");
        result.empty();
        result.append(html);
      };
    </script>
  </body>
</html>

まとめ

  • DBなどに貯まっているデータをJavascriptでグラフ表示することができました。
  • グラフの描画やグラフのオプションは、参考にさせていただいたサイトをみるととてもよくわかります(そのままのところも多々あります。感謝)
  • 次は、Vue.jsで動的に動くグラフの作成を実施してみたいと思います。

参考URL

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

Javascript入門書のソースコードを写経する際に工夫した方がいい3つのこと

未経験のプログラミング言語を学ぶ際は、大抵の人が

・入門書を手に入れる
・環境を整えてみる
・実際に入門書に書いてあることを打ち込んでみてその通り動くか確認する

という手順で開始すします。完全に初学者ならばそれでもいいと思いますが、もし過去に少しだけプログラミングを触ったことある人や、別言語経験者、言語は経験があるけど新しいフレームワークを学び始めた(たとえばJavascriptは経験していて、これからReactを学ぶなど)際は、それだけだと物足りない…というか大して学びはありません。

実際、progateでも、特に動的型付け言語間ならばある程度一つの言語で業務経験あるならば未経験の言語とはいえカリキュラムをこなすだけなら退屈です。

また、入門書の後半には多少実用的なソースコード(「TODOリスト作ってみました」等)があるケースが多いですが、そのままですと「関数化されていない」「変数がリークしてしまう」など、実務上ではレビューを通すのは難しいものが殆どです。

そこで、主にウェブ系のプログラムが対象ですが、もし入門書の写経が終わった人や、他言語・他フレームワークの経験がある方は、以下のことに工夫してみることをお勧めします。

ソースを関数化する

入門書のソースコードは、初学者が見やすいようにあまり関数化されていないことも多いですが、関数の書き方自体はほぼ必ず載っています。
そこで、写経したコードを可能な限り関数化してみましょう。
その際には、機能ごとに関数で束ねてみることを工夫します。多少細かくなりすぎるくらいでも大丈夫です。平文でべったりと同じ動作が3回以上行われるところは特に「関数化できないか」工夫してみてください。

その際、関数名などは横着せずに真面目に考えて下さい。早く「動くプログラム」を作ってみたいという気持ちになるのは凄く分かりますが、関数やコメントで横着をしたソースはそれだけで可読性を大きく下げ、レビューの遅延を招きます。どういう名前のつけ方がいいのかは、ぐぐってみてください。

Javascriptの場合、即時関数化して変数が漏れ出さないようにする

ほとんどの書籍ではjsファイルのソースコードを、チーム開発を想定して作っていません。
HTMLファイルにベタ書きか、jsファイル内でいきなりvarやletで変数を定義してコードを書いています。
そのままですと、他のjsファイルに対して変数が汚染されてしまいますので、チームで開発する際もさることながら自分で開発する際も、混乱を招くことがあります。

即時関数化するなどして変数が漏れないようにしましょう。そのやり方に関しては以前こちらに投稿しております。

https://qiita.com/Yuna0610/items/5886401d3b91cde8a8f5

機能ごとにファイルを分割する

実際の開発環境では、機能ごとにJsファイルを分割することも多々あります。
・初期化関数
・定数群
・テーブル描画
・グラフ描画
など、機能に分けてjsファイルを作成してみましょう。上記の項目に沿って即時関数化していたら、初学者にとっては変数を他のjsファイルに渡すところで何をやっているのか分からないかもしれませんが、最初はそれでも大丈夫です。

(可能ならば)要素はHTMLに書き込むのではなく、JavascriptやJQueryを利用してDOM挿入で書かせる

これは、初学者や未経験者にとっては難しいかもしれないので余力があればで大丈夫です。プルダウンリストやテーブル、ラジオボタンなどの要素はHTML上でidやclassを指定してベタ書きするのではなく、JavascriptやJQueryを用いてDOM挿入で書いてみましょう。
Javascriptを学んでいる段階ではJQueryを並行してマスターするのは大変でしょうが、こちらは必要な要素だけ拾い食いして必要なものだけピックアップしても使うことが出来ます。

ここまでやるのは確かにハードルが高いかもしれませんが、無理してやった分コーディングへの理解は確実に深まります。

以上、初学者がソースコードを写経する際に工夫した方がいいことをまとめました。
説明がJavascript寄りになってしまっていますが、少なくとも関数化に関しては他言語でもあてはまるかなと思っています。

また、すべての現場で上記のようなことがルールとなっているとは限りませんが、これらを自然に心がけることが出来るのでしたら、どんなルールにもすぐ適用できるようになるのかなと思います。

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

RailsにReactやVueはいらない? ajaxでviewを非同期で操作するgem(ActionPartial)を作りました。

ActionPartial

個人でCrover(クリエイターズプラットフォーム)というサービスを企画&開発&運営しているnirと申します。

Croverの開発で動的なviewの実装が必要になったので、gemを作ることにしました。

それで出来上がったのが、Railsで動的なviewを簡単に実現できるgemActionPartialです。

Crover(クリエイターズプラットフォーム)
https://crover.me

github(ActionPartial)
https://github.com/nir-searchright/actionpartial

なぜ、ReactVueを使わなかったかというと
- 学習コストが高い
- フレームワークにフレームワークを組み込むことに違和感がある
- 設計思想的な問題(RailsRailsのまま使いたい)

自分だけで使うはもったいないので、とりあえず公開することにしました。

注意)
- ReactVueを否定する記事ではありません。

DEMO

簡易的なデモを作成したのでご自由にお試しください。

デモ
https://actionpartial-demo.herokuapp.com

github(ActionPartial DEMO)
https://github.com/nir-searchright/actionpartial_demo

herokuの無料プランなのでデモのページを開くのに時間がかかるかもしれません。

ここから先はデモを元に説明します。

導入方法

Gemfile'actionpartial', github: 'nir-searchright/actionpartial'を追加してbundle installします。

現状、RubyGemsには登録していないのでgithuburlは必須です。

gem 'actionpartial', github: 'nir-searchright/actionpartial'

application_helper.rbrequireするだけで準備は完了です。

application_helper.rb
require 'action_partial'

ActionPatialができること

ActionPartialができることは

  • partialの更新
  • partialの追加
  • partialの削除

これらの単純な機能だけです。

単純故に応用も利きやすい設計となっているはずです。(多分)

demo_a.gif

仕組み

前提として、Railsではlink_toformremote: trueをつけるとajaxで通信が行われます。

<%= form_for(@post, remote: true) do |f| %>
  内容
<% end %>

ajaxでリクエストを送り、サーバーからxxx.js.erbを返します。

def create
  内容
  render 'posts/js_erb/create.js.erb'
end

サーバーから返されたxxx.js.erbview
- innerHTMLpartialを更新(https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML)
- insertAdjacentHTMLで任意の位置にpartialを挿入(https://developer.mozilla.org/ja/docs/Web/API/Element/insertAdjacentHTML)
- removepartialを削除(https://developer.mozilla.org/ja/docs/Web/API/ChildNode/remove)
などをしています。

helperメソッド一覧

html.erbで使うヘルパー

index.html.erb
<%= ap_render "posts/list/container", class: "posts-padding-bottom", locals: {posts: @posts} %>
<%= ap_render "posts/new" %>

<%= ap_init(path, options={}) %>

  • 最初は表示したくないけど、後からコンテンツを追加したい場所に使います。
  • エラーメッセージなどの後から表示する要素に使います。

<%= ap_render(path, options={}) %>

  • 非同期で更新したいpartialrenderするのに使います。
  • 基本的にRailsが提供するrenderと大して変わりません。

js.erbで使うヘルパー

※サンプルコードは下の個別の説明に載せてあります。

<%= ap_before(path, options = {}) %>

  • ap_initもしくはap_renderしたpartialの外側上部にpartialが挿入されます。

<%= ap_prepend(path, options = {}) %>

  • ap_initもしくはap_renderしたpartialの内側上部にpartialが挿入されます。

<%= ap_append(path, options = {}) %>

  • ap_initもしくはap_renderしたpartialの内側下部にpartialが挿入されます。

<%= ap_after(path, options = {}) %>

  • ap_initもしくはap_renderしたpartialの外側下部にpartialが挿入されます。

<%= ap_replace(path, options = {}) %>

  • ap_initもしくはap_renderしたpartialがサーバーから取得した新しいpartialに置換されます。

<%= ap_remove(id) %>

  • html上に存在する要素をid指定で削除します。

基本の使い方

基本は更新or追加or削除したいpartialid(ap_initもしくはap_renderした場合はpartialpath名)を指定して使います。

partialでインスタンス変数を使っている場合はlocalsで変数を渡します。

add.js.erb
<%= ap_append("posts/list/loop", locals: {posts: @posts}) %>

詳しくはDEMOのadd.js.erbをご確認ください。(このコードは無限スクロールのコードです。)
add.gif

他の要素やpartialpartialを追加(挿入)したい場合

追加したい要素やpartialid(ap_initもしくはap_renderした場合はpartialpath名)を指定することでpartialの追加(挿入)が可能です。

create.js.erb
<%= ap_prepend("posts/list/item", id: "posts/list/loop", class: "posts-list-item", locals: {post: @post}) %>

詳しくはDEMOのcreate.js.erbをご確認ください。(このコードは新規投稿のコードです。)

create_a.gif

複数のpartialから一つのpartialを指定したい場合

js.erbで使えるヘルパーはoptionidを指定することができます。

例えばhtml.erbでこんな感じでeachで回しながらコンテンツにidを指定することでjs.erbでコンテンツを指定することが可能です。

<% posts.each do |post| %>
  <div id="posts_<%= post.id %>">
    内容
  </div>
<% end %>

詳しくはDEMOのupdate.js.erb destroy.js.erb calcel.js.erbをご確認ください。

update_a.gif
delete_a.gif

jsの実行

当たり前と言えば当たり前ですがjs.erbjsのコードを書いておけば一緒に実行できます。

create.js.erb
<%= ap_prepend("posts/list/item", id: "posts/list/loop", class: "posts-list-item", locals: {post: @post}) %>

document.getElementById("post_content").value = "";
scrollTo(0, 0);

ただ、このjsが実行できてしまう仕組みがセキュリティ的に良いのかは分かりません。

理論上だとhttps通信をしているなら問題ないはずですが、どうなのでしょうか?

どなたか詳しい方がいればコメントで教えていただけると助かります。

最後に

今回やったことは「ネット上でよく見かけるjs.erbでの非同期更新を簡単に使えるようにgem化した」だけです。

作ってみて思ったよりも使いやすかったのでとりあえず公開しました。

ActionPartialは学習コスト低めでhtmljsrailsだけ理解していれば使えるのでスピーディーな開発ができると思います。

スピードを重視するスタートアップのプロジェクトとの相性が良いのではないでしょうか?

機能的にはシンプルなgemなので導入で不具合を吐くことはほとんどないと思います。

そのうち気が向いたらrails ujsを使ったコンテンツの非同期更新の記事もアップします。

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

RailsにReactやVueは不要!? ajaxでviewを非同期で操作するgem(ActionPartial)を作りました。

ActionPartial

個人でCrover(クリエイターズプラットフォーム)というサービスを企画&開発&運営しているnirと申します。

Croverの開発で動的なviewの実装が必要になったので、gemを作ることにしました。

それで出来上がったのが、Railsで動的なviewを簡単に実現できるgemActionPartialです。

Crover(クリエイターズプラットフォーム)
https://crover.me

github(ActionPartial)
https://github.com/nir-searchright/actionpartial

なぜ、ReactVueを使わなかったかというと
- 学習コストが高い
- フレームワークにフレームワークを組み込むことに違和感がある
- 設計思想的な問題(RailsRailsのまま使いたい)

自分だけで使うはもったいないので、とりあえず公開することにしました。

注意)
- ReactVueを否定する記事ではありません。

DEMO

簡易的なデモを作成したのでご自由にお試しください。

デモ
https://actionpartial-demo.herokuapp.com

github(ActionPartial DEMO)
https://github.com/nir-searchright/actionpartial_demo

herokuの無料プランなのでデモのページを開くのに時間がかかるかもしれません。

ここから先はデモを元に説明します。

導入方法

Gemfile'actionpartial', github: 'nir-searchright/actionpartial'を追加してbundle installします。

現状、RubyGemsには登録していないのでgithuburlは必須です。

gem 'actionpartial', github: 'nir-searchright/actionpartial'

application_helper.rbrequireするだけで準備は完了です。

application_helper.rb
require 'action_partial'

ActionPatialができること

ActionPartialができることは

  • partialの更新
  • partialの追加
  • partialの削除

これらの単純な機能だけです。

単純故に応用も利きやすい設計となっているはずです。(多分)

demo_a.gif

仕組み

前提として、Railsではlink_toformremote: trueをつけるとajaxで通信が行われます。

<%= form_for(@post, remote: true) do |f| %>
  内容
<% end %>

ajaxでリクエストを送り、サーバーからxxx.js.erbを返します。

def create
  内容
  render 'posts/js_erb/create.js.erb'
end

サーバーから返されたxxx.js.erbview
- innerHTMLpartialを更新(https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML)
- insertAdjacentHTMLで任意の位置にpartialを挿入(https://developer.mozilla.org/ja/docs/Web/API/Element/insertAdjacentHTML)
- removepartialを削除(https://developer.mozilla.org/ja/docs/Web/API/ChildNode/remove)
などをしています。

helperメソッド一覧

html.erbで使うヘルパー

index.html.erb
<%= ap_render "posts/list/container", class: "posts-padding-bottom", locals: {posts: @posts} %>
<%= ap_render "posts/new" %>

<%= ap_init(path, options={}) %>

  • 最初は表示したくないけど、後からコンテンツを追加したい場所に使います。
  • エラーメッセージなどの後から表示する要素に使います。

<%= ap_render(path, options={}) %>

  • 非同期で更新したいpartialrenderするのに使います。
  • 基本的にRailsが提供するrenderと大して変わりません。

js.erbで使うヘルパー

※サンプルコードは下の個別の説明に載せてあります。

<%= ap_before(path, options = {}) %>

  • ap_initもしくはap_renderしたpartialの外側上部にpartialが挿入されます。

<%= ap_prepend(path, options = {}) %>

  • ap_initもしくはap_renderしたpartialの内側上部にpartialが挿入されます。

<%= ap_append(path, options = {}) %>

  • ap_initもしくはap_renderしたpartialの内側下部にpartialが挿入されます。

<%= ap_after(path, options = {}) %>

  • ap_initもしくはap_renderしたpartialの外側下部にpartialが挿入されます。

<%= ap_replace(path, options = {}) %>

  • ap_initもしくはap_renderしたpartialがサーバーから取得した新しいpartialに置換されます。

<%= ap_remove(id) %>

  • html上に存在する要素をid指定で削除します。

基本の使い方

基本は更新or追加or削除したいpartialid(ap_initもしくはap_renderした場合はpartialpath名)を指定して使います。

partialでインスタンス変数を使っている場合はlocalsで変数を渡します。

add.js.erb
<%= ap_append("posts/list/loop", locals: {posts: @posts}) %>

詳しくはDEMOのadd.js.erbをご確認ください。(このコードは無限スクロールのコードです。)
add.gif

他の要素やpartialpartialを追加(挿入)したい場合

追加したい要素やpartialid(ap_initもしくはap_renderした場合はpartialpath名)を指定することでpartialの追加(挿入)が可能です。

create.js.erb
<%= ap_prepend("posts/list/item", id: "posts/list/loop", class: "posts-list-item", locals: {post: @post}) %>

詳しくはDEMOのcreate.js.erbをご確認ください。(このコードは新規投稿のコードです。)

create_a.gif

複数のpartialから一つのpartialを指定したい場合

js.erbで使えるヘルパーはoptionidを指定することができます。

例えばhtml.erbでこんな感じでeachで回しながらコンテンツにidを指定することでjs.erbでコンテンツを指定することが可能です。

<% posts.each do |post| %>
  <div id="posts_<%= post.id %>">
    内容
  </div>
<% end %>

詳しくはDEMOのupdate.js.erb destroy.js.erb calcel.js.erbをご確認ください。

update_a.gif
delete_a.gif

jsの実行

当たり前と言えば当たり前ですがjs.erbjsのコードを書いておけば一緒に実行できます。

create.js.erb
<%= ap_prepend("posts/list/item", id: "posts/list/loop", class: "posts-list-item", locals: {post: @post}) %>

document.getElementById("post_content").value = "";
scrollTo(0, 0);

ただ、このjsが実行できてしまう仕組みがセキュリティ的に良いのかは分かりません。

理論上だとhttps通信をしているなら問題ないはずですが、どうなのでしょうか?

どなたか詳しい方がいればコメントで教えていただけると助かります。

最後に

今回やったことは「ネット上でよく見かけるjs.erbでの非同期更新を簡単に使えるようにgem化した」だけです。

作ってみて思ったよりも使いやすかったのでとりあえず公開しました。

ActionPartialは学習コスト低めでhtmljsrailsだけ理解していれば使えるのでスピーディーな開発ができると思います。

スピードを重視するスタートアップのプロジェクトとの相性が良いのではないでしょうか?

機能的にはシンプルなgemなので導入で不具合を吐くことはほとんどないと思います。

そのうち気が向いたらrails ujsを使ったコンテンツの非同期更新の記事もアップします。

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

Promiseとリトライとリトライ制御とキャンセル

Promiseで処理が失敗したらリトライしたい

Promiseの結果次第でリトライをかけたい場合があります。簡単な実装は以下で、これはググるとすぐに出てきます。

function retry(func, retryCount) {
  let promise = func();
  for (let i = 1; i <= retryCount; ++i) {
    promise = promise.catch(func);
  }
  return promise;
}
// 失敗時に5回までリトライ
retry(() => fetch("url"), 5).then(...)

// これは以下と同じ
fetch("url")
  .catch(() => fetch("url"))
  .catch(() => fetch("url"))
  .catch(() => fetch("url"))
  .catch(() => fetch("url"))
  .catch(() => fetch("url"))
.then(...)

これは成功時は「実行してthenを処理」となるだけですが、失敗時は「catchで再実行を5回まで繰り返してthenを処理」します。

他にも凝った実装はStackover flowにまとまっているので、気に入ったものを選ぶとよいと思います。

https://stackoverflow.com/questions/38213668/promise-retry-design-patterns

これで十分?

リトライといってもただやり直すだけではなくて色々あると思います。今回は以下を考慮します。

  • リトライまでの時間をコントロールしたい(Retry-Afterヘッダを尊重したいときとか、Exponential Backoffを入れたいときとか)
  • 条件次第ではリトライしたくない(ステータスコード的にリトライが無意味な場合など)
  • 途中でキャンセルしたい

サンプルコード

jsfiddleに動くコードを書いたので、動きを見てみたい方はご覧ください。

https://jsfiddle.net/b2hcy0r7/

以下はjsfiddleが消えた時用

/**
 * 指定されたミリ秒だけPromiseの完了を待つ。
 * AbortSignalが渡されていたら、Signalの状態によってはキャンセル(reject)する。
 */
function sleepAsync(milliseconds, signal) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(resolve, milliseconds);
    if (signal) {
      const onabort = () => {
        clearTimeout(timer);
        reject();
      };
      if(signal.aborted){
        // イベント後に呼ばれると下のコールバックでは拾えないので、即座に呼び出してそのケースを考慮している
        onabort();
      }else{
        signal.addEventListener('abort',  onabort);
      }
    }
  });
}

/**
 * リトライ実装のベース
 */
function retry(func, retryDelay, determinationRetry, signal) {
  return new Promise((resolve, reject) => {
    const inner = (count) => {
      func().then(resolve, (...rejectArgs) => {
        if (determinationRetry(count, rejectArgs)) {
          sleepAsync(
            retryDelay(count, rejectArgs),
            signal
          ).then(
            () => inner(count + 1),
            () => reject(...rejectArgs) // キャンセル時は最後の失敗時の結果を返す
          );
        } else {
          reject(...rejectArgs);
        }
      });
    };
    inner(1);
  })
}

これだと普段使いにはちょっと扱いにくいので、関数で包んだものを作ります。
条件次第でリトライしたくない場合などは第3引数の関数の内容を書き換えればよいでしょう。

// リトライ実装その1
const retryN = (maxcount, func, signal) => retry(
  func,
  () => 1000,                   // 1秒待つ
  (count) => count <= maxcount, // リトライはmaxcount回まで
  signal
);

// リトライ実装その2
// 本格派リトライ(最大10秒+α待ち)
const retryExponentialBackoff = (maxcount, func, signal) => retry(
  func,
  (count) => Math.min(2 ** count * 100, 10 * 1000) + Math.random() * 1000, 
  (count) => count <= maxcount,
  signal
);

こんな感じに使います。

  const canncelButton = document.getElementById('cancel');

  // キャンセル用のオブジェクト(AbortControllerで代用)
  const abortController = new AbortController();
  abortController.signal.addEventListener('abort', () => {
    alert("キャンセル操作が行われました");
  });

  // ボタンを押してキャンセルしたとき
  const cancelEvent = (e) => {
    abortController.abort();
  };  
  canncelButton.addEventListener('click', cancelEvent);

  // doTaskAsync(successRate).then((...successedArgs) => { ...
  // とやっていた箇所を以下のように書き換える
  retryExponentialBackoff(
    5,
    () => doTaskAsync(successRate),
    abortController.signal
  ).then((...successedArgs) => {
    console.log(successedArgs)
  }, (...failedArgs) => {
    console.log(failedArgs)
  }).finally(() => {
    canncelButton.removeEventListener('click', cancelEvent);
  });

付録

AbortController

https://developer.mozilla.org/en-US/docs/Web/API/AbortController

AbortControllerはfetch APIのキャンセルに使ったりするものです。(fetch APIが出来てしばらくはキャンセルが出来なかった。XMLHttpRequestは出来ていた)

まだ試験的ですがモダンブラウザには実装されていますし、なんちゃって実装ならEventTargetを使って簡単にかけます。

window.AbortController = function(){
  const signal = new EventTarget();
  signal.aborted = false;
  return {
    signal,
    abort(){
      signal.aborted = true;
      signal.dispatchEvent(new Event('abort'));
    }
  }
};

最初は雑に実装して色々試していたんですが、sleep(setTimeout)中のキャンセルがスムーズにいかないのが気になって、簡単に実装できないか探したところたどり着いたのがAbortControllerでした。C#のCancellationTokenSourceに近いですね。

蛇足: fetchはHTTPステータスコードが5xx/4xxでもResolvedになる

上記の実装ではRejectedなときだけリトライするため、そのままfetchを使ったらリトライ処理になりません。
なので、例えば以下のようにしてあげる必要があります。

fetch("url").then((res) => res.ok ? res : Promise.reject(res))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む