- 投稿日:2020-02-12T23:47:06+09:00
【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
- 投稿日:2020-02-12T23:42:42+09:00
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とサービス>認証情報 を選択します。
アプリケーションの種類を「ウェブアプリケーション」と選択し、入力事項を埋めます。
作成が完了するとクライアント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/apiclientPOSTで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に飛んでいてくれるはず。
お世話になったサイト
- 投稿日:2020-02-12T23:31:59+09:00
制約の多い運用現場でエンジニアっぽいことをする方法
対象
- セキュリティの関係でソフトやツールをインストール・導入できない
- お金がないからツールを購入してもらえない
- サクラエディタでコードを書いてる
- そもそも周りにコードかいてるやつがいない
- 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で標準で入ってるものをうまく利用していくしかない.あのライブラリ使えたら楽だなーとかおもうこともあるが,利用できる環境じゃなければ自分で作っていくしかない.車輪の再発明かもしれないけど,この経験は無駄にはならないと思うので同じような境遇の方には是非試してもらいたい.またこの他に使えるものがあればぜひ教えてくださいー
- 投稿日:2020-02-12T23:27:40+09:00
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アプリの構築、テスト、展開について専門家が知っていることを学び、習得してください。
- 投稿日:2020-02-12T22:29:14+09:00
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=hNsmKullIvoSVGとは
スケーラブル・ベクター・グラフィックス(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と、いったところで。
現状報告終了!
- 投稿日:2020-02-12T21:25:56+09:00
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のダウンロードと登録
上記からExpoにアクセスし、SignUp します。
お使いのスマホがあれば、公式アプリをダウンロードします。Expoで開発画面を表示する
npm start
してブラウザに表示された画面に、QRコードがあればそれをスマホで読み取ることで開発中の画面を表示することができます。ここでエラー!
私の場合はここでエラーがでました。
error Unable to resolve "@react-navigation/native" from "App.js"うーん、初めてなのでなんのこっちゃモジュール。
ひとまず
App.js
を公式チュートリアルのとおり、シンプルに書き直すことで解決しました。App.jsimport 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.jsimport 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までたどりつきました。
React Native独自のコンポーネント
通常のJSXで使うことができる
<div>
や<h1>
などのコンポーネントの代わりに<View>
や<Text>
を使います。- <View> : <div>や<span> - <Text> : <p>など文字列の表示に使用画像を表示する
「Hello, World!」の代わりに画像を表示します。
React Nativeでは<img>
のかわりに<Image>
が用意されているので、インポートして使います。画像ソースを指定するプロパティは、
src
ではなくsource
です。App.jsimport 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 HelloWorldAppPropsとStateは通常のReactと同じ
同じ...だと思います。
まとめ
...更新中
- 投稿日:2020-02-12T21:25:34+09:00
【初心者向け】JavaScriptのfor文でHTMLに表示する九九表を作る!
JavaScriptでHTMLに表示するtableを作る!
完成形はこんな感じ
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文でやってること
2,3番目のfor文でやってること
要はこの行の掛け算( j*k )を9回繰り返して下の画像のようにしてる
おわりに
雰囲気for文を使った掛け算のやり方がわかったでしょうか、、?
htmlでテーブルを作ろうとすると逆にtrの中にth,tdをどのように配置すればいいのかわからないことがあるので繰り返しのテーブルを作る際にはJavaScriptの方がわかりやすいこともあるかもしれないですね!
日常でこの九九表を作ることに意味は感じませんが、for文の中にfor文があるという時の練習にはなるのではないでしょうか。。
私もよくわかってませんが、何かの参考になれば幸いです!(雰囲気なので間違っていたりもっといい伝え方があれば教えていただきたいです、、ごめんなさい、、、)
今後、計算結果で偶数の時は色を変える、、の処理もいつか記事にしたいと思います
お疲れ様でした!
- 投稿日:2020-02-12T19:47:57+09:00
自分用コードメモ - 特定の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 アカウント
- 投稿日:2020-02-12T19:27:22+09:00
自分用コードメモ -特定の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 アカウント
- 投稿日:2020-02-12T18:41:04+09:00
すっきり書きたい 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.findarray.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には色々な構文がありますが、それぞれの長所・短所を踏まえつつ、すっきりしたコーディングを目指していけると良いのかな、と思います。
以上です。
間違い、不足点の指摘などあれば、コメントをお願いいたします。
- 投稿日:2020-02-12T18:01:17+09:00
PlayCanvasで建築系のモデルを良い感じの3Dビュワーにしてみる [WebGL]
PlayCanvasの非ゲーム分野を開拓中のキドです。
今回はビジュアライゼーションで建築系に使えそうなコンテンツを作ってみました。
建築系ビジュアライズ
ここでコンテンツを見ることができます
https://playcanv.as/p/F1ZzFOYS/とあるビルのオフィス内の案内コンテンツ、という感じで作りました。
3Dコンテンツなのでカメラで外装を見たり、階層の番号をクリックすることでその階層の中を一望できたり。
あとはそれに応じたDOMも変更したり、レイキャストでクリックできるスポットを設置したりしました。FlashがWebでよく使われていた頃に、デパートとか施設のマップ案内をするWebページがありました。
その頃はFlashで実装されていましたが、2020年いっぱいでFlashも終わってしまいます。
今では画像だとかpdfだとかで実装されていて、若干見にくくなった気がしますね…そんな施設のマップ案内をFlashの代わりになりそうですよ。
作り方
今回の記事では完成したプロジェクトから紹介しているだけなので、丸々コピーしても正しく動作しない場合があるのでご了承を。
また、コードの説明については軽く触れますが詳しく説明しませんのでこちらもご了承を。
レシピ
今回は以下の3つ用意すればおk
- PlayCanvas
- 使用する3Dモデル
- Vue.js (
https://cdn.jsdelivr.net/npm/vue/dist/vue.js
)- PlayCanvas Tweenライブラリ
PlayCanvasから新規プロジェクトを作る際はテンプレートに「Model Viewer Starter Kit」を選んでください。
3Dモデルを配置
用意した3Dモデルは階層ごとにあらかじめ分けておきます。
今回はLightを使用せず、3Dモデルに陰影を焼き付けてから使用しています。HierarchyでのEntityの配置の仕方は、buildingを親として子に各階層を入れています。
3Dモデルが移動する位置を決める
先ほど配置した3DモデルのEntityの子に、横にスライドしたときの位置を決めたEntityを配置します。
同じ3Dモデルを使って位置を決めるとわかりやすくて良いですね。決められたらこのEntityはEnabledをfalseにして非表示しておきます。
3Dモデルの配置ができたら、次はCameraも移動する位置を決めます。
横にスライドした3Dモデルを俯瞰できるような位置に設置して、これもEnabledをfalseにして非表示にします。
Cameraの設定
テンプレートから作っていれば以下のScriptsがセットされていると思います。
- orbitCamera.js
- mouseInput.js
- touchInput.js
これらはこのままで。
orbitCamera.js
のFocus Entityを変更しますが、その原点となるEntityを設置します(2枚目の画像)
ここではorbitcamera-gentenと名前をつけていますが、原点にしか使わないのでなんでも良いです。
Focus Entityが変更できたらOKです。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を使います。
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 = ""; } } }); };私はこういうDOMを操作する系のスクリプトはRootに毎度のごとく登録しています。
特に意味はないですが、なんとなく早く読み込んでほしいと思いと見やすいし管理しやすいからですね。
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を追加します。
配置は真上から見て良い感じの場所に配置させておきましょう。
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の良いところですよね。背景透過
ここまでで処理は完成していますが、ついでにPlayCanvasのコンテンツ背景を透過させようと思います。
Cameraの設定のClear ColorのAlphaを0に。
SETTINGSのRENDERINGの設定内にあるTransparent Canvas
をtrueにします。PlayCanvasでのcanvasの透過方法ですが、これも別記事で説明しています。
詳しくはそちらでご参照ください完成
ここまで設定できたら完成です!
Vue.jsを使うことでデータの受け渡しが楽になるので良いですね。
Vue.jsの良いところはガチガチのフレームワークではなくライブラリ感覚で使えるところでしょうか。
今回はPlayCanvasの中でVue.jsを使ってみましたが、他のjsライブラリでも試して見るもの良いですね。
処理のフロー
コードについてあまり説明をできていませんが、流れについて説明しておきましょう。
画像は簡単なものなのでわかりづらいですが石とか投げないでください。
今回の大元は
changeFloor.js
ですが、基本的にはopenする階層を選択したら移動するというのが主な仕事です。
これを行うためには、各それぞれの要素がどんな動きをしているのか、どんな状態で待機しているのかを管理する必要がありました。
今回で言えば、autoRotate.js
は自動回転を始めてしまいますから、階層をopenにした後にも自動回転してしまっては思った挙動にならないです。
DOMについても表示すべき場面で表示させないといけません。
そのため、changeFloor.js
は名前的には階層を変える処理を行うのですが、他の処理をコントロールする中枢の処理を担っています。反省
なるべく管理のしやすい、見やすいを心がけて作りましたが、コードはあんまり綺麗にできませんでした…
ゲーム開発はやったことないのでどうするのが一番良いのかわからないですが、なるべく誰でも見た感じでわかるようなプロジェクトを作っていきたいですね。
- 投稿日:2020-02-12T17:32:26+09:00
【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を投入するほか、
添付フィールドを用意したので、ここから画像を選んでも動作します。
<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() }以上です。
- 投稿日:2020-02-12T17:28:32+09:00
Wordpressの管理画面にオリジナルのCSS・JavaScriptを適用させる
フォルダ構成
必要な記述
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');
- 投稿日:2020-02-12T17:23:01+09:00
PlayCanvasの背景を透過canvasにする方法
canvasの背景透過
HTML5のcanvas要素は毎フレームでレンダリングを行います。
canvasの背景を透過したいなんて声はググったりすると意外と多い気がします。ちなみに、canvasの背景を透過したいなんて時は以下のようにalpha値を指定してあげれば良いです。
gl.clearColor(0, 0, 0, 0)PlayCanvasの背景透過
PlayCanvasはどうしたらいいか。
PlayCanvasエディターのRENDERINGにTransparent Canvasという項目があります。
これにチェックを入れます。これだけで背景は透明になったかというとそうでもなかったり…
EntityにCameraがある場合は、このCameraのClear ColorのAlpha値を0にしてあげるとちゃんと透過されます。
また、SkyBoxを入れている場合もちゃんと透過されないのでご注意を。
これでCSSからbackground-colorを変更してあげると透過されているのがわかります。
Webで使う際には透過はよく使うので覚えておきたいですね。
- 投稿日:2020-02-12T13:48:28+09:00
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;
で既定の動作をキャンセル可能かどうか。上の表で
★
を付けたキーの機能はキャンセルできませんでした。
- 投稿日:2020-02-12T12:38:56+09:00
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>
- 投稿日:2020-02-12T12:15:01+09:00
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オプションが必要なようです。
- 投稿日:2020-02-12T12:04:03+09:00
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)); } } }参考サイト
目次
- 投稿日:2020-02-12T12:02:49+09:00
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 Firestore
やCloud Storage
やその他サービスへ渡すしかない実行時間に制限がある
- 最大9分
- そのためバッチ処理には不向き
ユーザー側からの呼び出し以外に、
Firebase
の他サービスからの通知によって実行することもできる
- ファイルが
Cloud Storage
にアップロードされた時
- 自動でサムネイルを作るとか
Authentication
でユーザーが作られた時
- 自サービス用のユーザーデータを
Firestore
に放り込むとか- 定期実行も可能
- 1時間毎とか1日毎とか
関数毎にメモリの割り当てを指定することができる
- 環境変数なども指定できる
Firebase
のコンソールからではなく、Google Cloud Platform
のコンソールから指定するFirebase
といいつつGoogle Cloud Platform
のCloud Functions
と同じなので、Google Cloud Platform
側の画面で色々できる
StackDriver
でログを制御する、とか結構遅い
- レスポンスまでに1秒くらいは見ておいた方がいいかも?
- 特に内部的にインスタンスが立ち上がる時が遅い
- しばらくアクセスがなくて久しぶりにアクセスが会った時とか
- 5秒くらいかかるイメージ
Firestore
からデータを取得する時にループを回して、中でawait
とかすると凄まじく遅くなる
- ループではなく並列取得するべき
参考サイト
目次
- 投稿日:2020-02-12T12:02:05+09:00
Google Firebase まとめ
Firebase
について、まとめていきます。はじめに
中級者が気軽にパッと見てある程度の内容が把握できる、くらいのところを目標に記述していきます。初心者向けではないです。
なるべくわかりやすくするため、基本的に短文箇条書き形式にて記します。
サンプルコードが必要なところはJavaScript
にて記載します。間違いの指摘などあれば、コメントお待ちしております。
Firebaseとは?
- アプリケーションのサーバーサイドを楽に作れる仕組み
- 基本的にサーバーレス
- 例えば以下のような物がサーバーレスで利用できる
- データベース(Firestore)
- ファイルサーバー(Storage)
- API(Cloud functions)
- Webサーバー(Hosting)
サーバーレスとは?
その名の通り、サーバーという概念が無い
- 実際には裏には存在しているが、意識しなくてよいようになっている
- イメージ的には、ファイルやアプリケーションのプログラムをアップローダーからアップロードするだけで動くようになるよ、的な感じ
メリット
- メンテナンスの手間が少ない
- セキュリティパッチやディスクの故障など何も心配する必要がない
- アクセスが急激に増えても自動スケールアウトで勝手にうまく処理してくれる
デメリット
- 従量制なのでかかる費用がよくわからない
- 基本的にはオンプレよりは安くなるはず?
- あまり細かい設定はできない
- 特殊な要件だと対応できないかも
- パフォーマンスチューニングとか
- 一台ごとの性能云々はあまり調整できないが、自動スケールアウトでお金で解決
- レイテンシが問題になることはあるかも?
主要サービスの概要
Cloud Functions
- アプリケーションを実行するためのもの
- 主に
API
を作るために利用できる- 言語は
JavaScript
、Python
、Go
- 手元でコードを書いて、アップロードコマンドを実行することで、
CloudFunctions
に配置できる
- 指定のURLを開く等で実行できる
Cloud Firestore
- KeyValueのデータベース
Cloud Functions
から簡単に利用できる- データの更新をリアルタイムに取得できる
Authentication
- 自作サービスのユーザー登録機能を自分で作らなくてよくするためのもの
- SMSやE-mailによる本人確認
- SNSによる本人確認
Firebase
のその他の機能と連携できる
Authentication
のユーザー情報を利用してCloud Functions
の呼び出しを制限するAuthentication
のユーザー情報を利用してFirestore
やCloud storage
のデータへのアクセスを制限するCloud Storage
- いわゆるファイルサーバー
- サーバーレスなので勝手にスケールする
- 同時接続数とか考えなくても良い
CloudFunctions
からアップロードされたファイルを保存したり等に利用できるHosting
- いわゆるWebサーバー
- サーバーレスなので勝手にスケールする
- 同時接続数とか考えなくても良い
- 静的なファイルをホスティングできる
Webでよくある開発パターン
CloudFunctions
でサーバーサイドのAPIを作る
CloudFunctions
からFirestore
へデータベースのデータを格納する- ファイルは
CloudStorage
へ- クライアントサイドを
SPA
で作る
- 作った
SPA
をHosting
でWebに配置SPA
からはCloudFunctions
のAPI
を叩きに行く各サービス毎のまとめ
以下にまとめて行きます。
- Cloud Functions まとめ
- Cloud Firestore まとめ
- Cloud Storage まとめ(準備中)
- Authentication まとめ(準備中)
- Hosting まとめ(準備中)
参考サイト
- 投稿日:2020-02-12T11:41:40+09:00
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
ファイル構成
前準備
以下の
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_tmpWebpackの設定
次にWebpackの設定ファイルとなる,
webpack.config.js
を作成していきます。webpack.config.jsconst 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-loader
→babel-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/390647a7c26933940470AutopreFixerの設定
AutopreFixer
はPostCSSの機能ですので,
利用できるようPostCSSの設定をpostcss.config.js
に記述していきます。postcss.config.jsmodule.exports = { plugins: [ require("autoprefixer")({ grid: "autoplace", browsersList: ["ie >= 11"] }) ], };ビルドについて
以上の設定ファイルを作成したのち,
npm run build
を実行することで,
エントリーポイントであるMain.tsx
とsrc配下のファイル
を
読み込んでビルドを行い,dist
以下に出力することができます。また,
npm run start
でリアルタイムでビルドを行い,
出力されたファイルが読み込まれた, ローカルサーバが
htttp://localhost:8080
で起動します。おわりに
間違えや質問などありましたら,
お気軽にコメントしていただけると幸いです。
今回はTypeScriptのテンプレートについては端折っているため,
そちらの記述方法などは,
GitHubを参照していただけたらと思います。
- 投稿日:2020-02-12T09:33:02+09:00
【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の場合
しかし、Safariで同じことをやろうとすると、次のようなエラーを吐いてしまいます。
The string did not match the expected pattern.
これはブラケットをしっかり閉じてあげるだけで解決します。
foo.html<input type="text" value="hogehoge" /> <script> const element = document.querySelector("input[type='text']") console.log(element.value) </script>どうやら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
普通のブラウザだと正常に動作するのも相まって気が付きにくですね
- 投稿日:2020-02-12T08:32:48+09:00
Chart.jsを使用して複数軸のグラフを表示する(Javascript)
はじめに
こちらの投稿(【Qiita API】いろんな方法で Views、Likes、Stocksを取得(JavaScript、Google Script、Python, Vue.js)で、Qiitaの自分の投稿について、QiitaAPIの情報を取得し、「ページビュー(View)」・「いいね(Like)」「ストック(Stock)」などをテーブル表示をさせました。
そのときに、定期実行(1日1回)にて、データベースに毎日の情報を貯めていました(Python使用)。そのデータを使って、視覚的に見れるように、グラフ表示(複数軸)をさせようと思いました。
やりたいこと
<グラフ表示>
・現在日付から過去1カ月のデータを取得する。
・取得したデータをchart.jsにてグラフ表示する。
(X軸:日付、Y軸1-棒グラフ:Qiita-View数、Y軸2-折れ線グラフ:Qiitaいいね・ストックの合計)<テーブル>
・取得したデータの中で最新日付のデータをテーブルを表示する。使用技術
- Javascript
- Chart.js
デモサイト
※これをみると、投稿がまったく見られないなんてことはなく、毎日少なからずViewがあるんだなぁと思います。(さすがQiita)
ポイント
- グラフの描画部分
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
- 投稿日:2020-02-12T08:25:52+09:00
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寄りになってしまっていますが、少なくとも関数化に関しては他言語でもあてはまるかなと思っています。また、すべての現場で上記のようなことがルールとなっているとは限りませんが、これらを自然に心がけることが出来るのでしたら、どんなルールにもすぐ適用できるようになるのかなと思います。
- 投稿日:2020-02-12T08:08:37+09:00
RailsにReactやVueはいらない? ajaxでviewを非同期で操作するgem(ActionPartial)を作りました。
ActionPartial
個人でCrover(クリエイターズプラットフォーム)というサービスを企画&開発&運営しているnirと申します。
Croverの開発で動的な
view
の実装が必要になったので、gem
を作ることにしました。それで出来上がったのが、
Rails
で動的なview
を簡単に実現できるgem
のActionPartial
です。Crover(クリエイターズプラットフォーム)
→ https://crover.megithub(ActionPartial)
→ https://github.com/nir-searchright/actionpartialなぜ、
React
やVue
を使わなかったかというと
- 学習コストが高い
- フレームワークにフレームワークを組み込むことに違和感がある
- 設計思想的な問題(Rails
をRails
のまま使いたい)自分だけで使うはもったいないので、とりあえず公開することにしました。
注意)
-React
やVue
を否定する記事ではありません。DEMO
簡易的なデモを作成したのでご自由にお試しください。
デモ
→ https://actionpartial-demo.herokuapp.comgithub(ActionPartial DEMO)
→ https://github.com/nir-searchright/actionpartial_demo
heroku
の無料プランなのでデモのページを開くのに時間がかかるかもしれません。ここから先はデモを元に説明します。
導入方法
Gemfile
に'actionpartial', github: 'nir-searchright/actionpartial'
を追加してbundle install
します。現状、RubyGemsには登録していないので
github
のurl
は必須です。gem 'actionpartial', github: 'nir-searchright/actionpartial'
application_helper.rb
でrequire
するだけで準備は完了です。application_helper.rbrequire 'action_partial'ActionPatialができること
ActionPartial
ができることは
partial
の更新partial
の追加partial
の削除これらの単純な機能だけです。
単純故に応用も利きやすい設計となっているはずです。(多分)
仕組み
前提として、
Rails
ではlink_to
やform
にremote: 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.erb
でview
に
-innerHTML
でpartial
を更新(https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML)
-insertAdjacentHTML
で任意の位置にpartial
を挿入(https://developer.mozilla.org/ja/docs/Web/API/Element/insertAdjacentHTML)
-remove
でpartial
を削除(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={}) %>
- 非同期で更新したい
partial
をrender
するのに使います。- 基本的に
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削除したい
partial
のid
(ap_init
もしくはap_render
した場合はpartial
のpath名
)を指定して使います。
partial
でインスタンス変数を使っている場合はlocals
で変数を渡します。add.js.erb<%= ap_append("posts/list/loop", locals: {posts: @posts}) %>詳しくはDEMOの
add.js.erb
をご確認ください。(このコードは無限スクロールのコードです。)
他の要素や
partial
にpartial
を追加(挿入)したい場合追加したい要素や
partial
のid
(ap_init
もしくはap_render
した場合はpartial
のpath名
)を指定することでpartial
の追加(挿入)が可能です。create.js.erb<%= ap_prepend("posts/list/item", id: "posts/list/loop", class: "posts-list-item", locals: {post: @post}) %>詳しくはDEMOの
create.js.erb
をご確認ください。(このコードは新規投稿のコードです。)複数の
partial
から一つのpartial
を指定したい場合
js.erb
で使えるヘルパーはoption
でid
を指定することができます。例えば
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
をご確認ください。jsの実行
当たり前と言えば当たり前ですが
js.erb
にjs
のコードを書いておけば一緒に実行できます。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
は学習コスト低めでhtml
とjs
とrails
だけ理解していれば使えるのでスピーディーな開発ができると思います。スピードを重視するスタートアップのプロジェクトとの相性が良いのではないでしょうか?
機能的にはシンプルな
gem
なので導入で不具合を吐くことはほとんどないと思います。そのうち気が向いたら
rails ujs
を使ったコンテンツの非同期更新の記事もアップします。
- 投稿日:2020-02-12T08:08:37+09:00
RailsにReactやVueは不要!? ajaxでviewを非同期で操作するgem(ActionPartial)を作りました。
ActionPartial
個人でCrover(クリエイターズプラットフォーム)というサービスを企画&開発&運営しているnirと申します。
Croverの開発で動的な
view
の実装が必要になったので、gem
を作ることにしました。それで出来上がったのが、
Rails
で動的なview
を簡単に実現できるgem
のActionPartial
です。Crover(クリエイターズプラットフォーム)
→ https://crover.megithub(ActionPartial)
→ https://github.com/nir-searchright/actionpartialなぜ、
React
やVue
を使わなかったかというと
- 学習コストが高い
- フレームワークにフレームワークを組み込むことに違和感がある
- 設計思想的な問題(Rails
をRails
のまま使いたい)自分だけで使うはもったいないので、とりあえず公開することにしました。
注意)
-React
やVue
を否定する記事ではありません。DEMO
簡易的なデモを作成したのでご自由にお試しください。
デモ
→ https://actionpartial-demo.herokuapp.comgithub(ActionPartial DEMO)
→ https://github.com/nir-searchright/actionpartial_demo
heroku
の無料プランなのでデモのページを開くのに時間がかかるかもしれません。ここから先はデモを元に説明します。
導入方法
Gemfile
に'actionpartial', github: 'nir-searchright/actionpartial'
を追加してbundle install
します。現状、RubyGemsには登録していないので
github
のurl
は必須です。gem 'actionpartial', github: 'nir-searchright/actionpartial'
application_helper.rb
でrequire
するだけで準備は完了です。application_helper.rbrequire 'action_partial'ActionPatialができること
ActionPartial
ができることは
partial
の更新partial
の追加partial
の削除これらの単純な機能だけです。
単純故に応用も利きやすい設計となっているはずです。(多分)
仕組み
前提として、
Rails
ではlink_to
やform
にremote: 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.erb
でview
に
-innerHTML
でpartial
を更新(https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML)
-insertAdjacentHTML
で任意の位置にpartial
を挿入(https://developer.mozilla.org/ja/docs/Web/API/Element/insertAdjacentHTML)
-remove
でpartial
を削除(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={}) %>
- 非同期で更新したい
partial
をrender
するのに使います。- 基本的に
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削除したい
partial
のid
(ap_init
もしくはap_render
した場合はpartial
のpath名
)を指定して使います。
partial
でインスタンス変数を使っている場合はlocals
で変数を渡します。add.js.erb<%= ap_append("posts/list/loop", locals: {posts: @posts}) %>詳しくはDEMOの
add.js.erb
をご確認ください。(このコードは無限スクロールのコードです。)
他の要素や
partial
にpartial
を追加(挿入)したい場合追加したい要素や
partial
のid
(ap_init
もしくはap_render
した場合はpartial
のpath名
)を指定することでpartial
の追加(挿入)が可能です。create.js.erb<%= ap_prepend("posts/list/item", id: "posts/list/loop", class: "posts-list-item", locals: {post: @post}) %>詳しくはDEMOの
create.js.erb
をご確認ください。(このコードは新規投稿のコードです。)複数の
partial
から一つのpartial
を指定したい場合
js.erb
で使えるヘルパーはoption
でid
を指定することができます。例えば
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
をご確認ください。jsの実行
当たり前と言えば当たり前ですが
js.erb
にjs
のコードを書いておけば一緒に実行できます。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
は学習コスト低めでhtml
とjs
とrails
だけ理解していれば使えるのでスピーディーな開発ができると思います。スピードを重視するスタートアップのプロジェクトとの相性が良いのではないでしょうか?
機能的にはシンプルな
gem
なので導入で不具合を吐くことはほとんどないと思います。そのうち気が向いたら
rails ujs
を使ったコンテンツの非同期更新の記事もアップします。
- 投稿日:2020-02-12T04:25:09+09:00
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))