- 投稿日:2019-12-13T23:11:21+09:00
非Javascriptエンジニアのための「Javascriptの制御構文」
前回の記事 - 非Javascriptエンジニアのための「Javascriptの比較演算子」
ITエンジニアのアクマちゃん(@akumachanit)デモ!
この記事は非JavascriptエンジニアのためのJavascript入門 3日遅れのひとり Advent Calendar 2019の記事デモ!この記事はJavaScript 「再」入門をベースに書いているデモ。
1日サボってしまったデモ...!気を取り直して今日は制御構文についてデモ!
今回紹介する制御構文
- 空文
- ブロック文
- ラベル文
- break文
- continue文
次回紹介するもの
- if...else文
- for文
- switch文
ループ構文は次回、エラー処理の
try
文はいつかエラーについてやる時に説明するデモ。空文
;
空文は、文が必要な箇所で文を明示的に「書かない」時に使う文デモ。
わけがわからないデモね。例を出すデモ。for (i = 0; i < arr.length; arr[i++] = 0);
for
文の後にはループ内で処理する文が期待されているデモ。
それを書かないことでfor
のカッコ内の処理(正確にはカッコ内の3つ目の式)だけを実行できるデモ。「文が期待されている場所で文を書かない」
これは次の項のブロック文の「1つの文しか書けない箇所に複数の文を書く」というのと対の機能を持つと言えるデモね。ブロック文
ブロック文は
{}
で囲んだ0個以上の文をグループ化することができるデモ。{ let a = 1; let b = 2; }変数の回で紹介した通り、
let
,const
はブロックスコープを持っているデモが、var
はブロックスコープを持っていないので注意デモ!let foo = 1; { let foo = 2; } console.log(foo); // 1 var bar = 1; { var bar = 2; } console.log(bar); // 2ブロック文は後述の
if...else
文やwhile
文、for
文などの制御フロー文と使われることが多いデモ。let a = false; if (a) { console.log("foo"); // 実行されない console.log("bar"); // 実行されない }ブロック文を使わないと1文しか制御できないデモね。
let a = false; if (a) console.log("foo"); // 実行されない console.log("bar"); // 実行されてしまうラベル文
ラベル文は文に予約語ではない任意のラベルをつけられるデモ。
block1: { // block1とラベル付けされたブロック文 } branch: if (true) { // branchとラベル付けされたif文 }ラベル文は後述の
break
とcontinue
で指定することができるデモ。break文
break
文を使用するとループ、switch
文、ラベル文を中断して次の処理に制御を移すことができるデモ。
while
文を中断してみるデモ。while (true) { if (a) { break; // whileが中断されて次の処理に移行します。 } }
a
がtrue
になるとループから抜けるデモ。次はラベルを指定してみるデモ。
lab1: { lab2: { lab3: { console.log('begin') break labX; //ここでlab1~3を指定 console.log('1'); } console.log('2'); } console.log('3'); } // break lab1を指定した場合の出力 'begin' // break lab2を指定した場合の出力 'begin' '3' // break lab3を指定した場合の出力 'begin' '2' '3'指定したラベルのブロック文が中断されて次の処理に移っているデモね。
continue文
continue
文はループの現在反復中の文(ブロック)を終了して、次の反復をスタートするデモ。ラベルを指定している場合は、ラベルが指定されているループの次の反復をスタートするデモ。
具体的には、while
文では条件式にジャンプするデモ。for
文では更新式までジャンプするデモ。while (true) { console.log('loop'); if (true) { break; } } console.log('finish.');あとがき
今回は
if...else
、for
とwhile
以外の制御構文を紹介したデモ。
今回紹介したブロック文やbreak
,continue
文は次回の制御構文で活躍するデモね!参考
- 投稿日:2019-12-13T23:04:25+09:00
複数のNuxt.jsを用いたプロジェクトに携わってきて感じたこと
みなさんこんにちは@y_temp4です。
自分は現在フリーランスのエンジニアとして主にフロントエンドの開発を行っており、これまでに複数の Vue/Nuxt の案件に携わってきました。
というわけで今回は、いくつかの Nuxt プロジェクトに関わってきて得た知見についてシェアしていければなと思います!
想定読者
この記事は以下のような方を想定読者として書いています。
- Nuxt を導入すべきか迷っている/新規プロジェクトで Nuxt の利用を検討している
- Nuxt の構成で迷っている
- Nuxt の利用について興味がある
Nuxt でプロジェクトを初める条件
まずはじめに Nuxt のプロジェクトを初める条件ですが、基本的に Nuxt はフロントエンドのフレームワークであり、バックエンドと疎結合な作りになっていることが望ましいです。いわゆる SPA を作る感じですね。
ですので、新規にプロジェクトを立ち上げる際は、バックエンドは完全に API サーバーとしてフロントエンドと分離させるような設計にすべきでしょう。
例えば、自分が初めて Nuxt のコードを書いた際はバックエンド API が AWS の Lambda + API Gateway でした。
参考:【ALIS のシステム】サーバサイドアーキテクチャ:その1 〜ALIS サーバサイド構成〜 | ALIS
余談:なぜ SPA で開発するのか
本記事のメインの話題とは話が逸れるので詳しくは書きませんが、SPA で開発する必要性・メリデメに関しては、以下のスライドが参考になるかと思います。
私たちはなぜ SPA で開発するのか / Why you choose SPA - Speaker Deck
Nuxt プロジェクトの構成で意識すべきこと
Nuxt のプロジェクトをいくつか見てきた感想として、自分が一番思うのは「Nuxt の仕組みに沿った形で作ったほうが良い」ということです。まぁフレームワークを使う上で、フレームワークの仕組みに乗っかったほうがいいのは当然かも知れませんが、フロントエンドの構成はよりバックエンドよりもおざなりになりがちかなと感じています。
例えば Nuxt にはモジュールという概念があり、これを用いることによってある程度品質の担保された機能を実装できます。
また、Nuxt のプロジェクトに元から用意されたディレクトリ構造くらいは守るようにしましょう。
Nuxt は変化の速度が早いフレームワークであるので、独自に実装した箇所がバージョンアップの追従の足枷になるケースがよくあります。ですので、Nuxt の機能を逸脱したことをやろうとする場合、それにそこまでする価値があるか・既存の仕組みで実現できないか、よく検討した方が良いと感じています。
TypeScript の導入
Nuxt における TypeScript の導入は、基本的に以下の公式ドキュメントに沿えば OK です。
しかし、それ以外の細かい点で TypeScript 導入を行う際につまずいたポイント等があるので、今回はそれについて列記します。
Nuxt をプログラムで使う際の設定
「Nuxt.js をプログラムで使う」にあるように、通常の
nuxt-ts
コマンドではなく Express などから Nuxt を動かしているケースでは、@nuxt/typescript-build
の関係でうまく動作しないことがありました。おそらくすぐに改善されるかとは思いますが、この記事の執筆時点での解決方法を記述しておきます。
import tsModule from '@nuxt/typescript-build' ... if (config.dev) { await nuxt.moduleContainer.addModule(tsModule) const builder = new Builder(nuxt) await builder.build() } else { await nuxt.ready() } ...メインの部分以外はよくあるコードと同じなので省略します。このコードはtypescript-build/test/module.test.tsを参考に書きました。
Lint について
見落としがちですが、Nuxt での eslint の設定は
@nuxtjs/eslint-config
、もしくは TS を使う際は@nuxtjs/eslint-config-typescript
を使うと、.eslintrc
をスッキリとした形で記述できます。.eslintrc.jsmodule.exports = { extends: ['@nuxtjs/eslint-config-typescript'] }ただ、2019 年 11 月に TypeScript の 3.7 が出ましたが、このパッケージのアップデートは TS3.7 への対応が少し遅く、ちょっとだけやきもきしたのを覚えています。
今後 TS 等の追従に伴い
@nuxtjs/eslint-config-typescript
のアップデートが待てない方は、中の実装まで一応確認しておいたほうがいいかもしれません。さいごに
Nuxt のプロジェクトに関わってきて思ったのは、やはり Nuxt は比較的簡単にはじめられる反面、フロントエンドのよくある規則等を把握しておかないとすぐに破綻するリスクもある、ということです。
今後、Vue に関しては Vue のバージョン 3 にてComposition APIが導入されこれが使われるようになると、おそらくより設計力が求められるようになるのではないか、と感じています。
Nuxt のプロジェクトでも Composition API を考慮した実装が追加されていくでしょうから、この辺は意識しておく必要があります。
個人的に Composition API は今ある Vue の問題(主に型周り)を解決しうる素晴らしい仕組みかと思いますが、規約等をきちんと決めて開発を始めないと既存のコードよりも実装のつらみが出てくることが予想されます。
参考:Vue Composition API のコラムっぽいもの集 - mya-ake com
繰り返しになりますが、今後 Nuxt の開発をする方はこの辺の流れの変化にも対応できるように意識しつつ開発を進めていくことをオススメします!
さいごに、この記事が参考になった方はぜひいいねしていただけますと幸いです!
最後まで読んでいただき、ありがとうございました ?
- 投稿日:2019-12-13T21:26:52+09:00
【ALB】さくっと5秒で簡単にメンテナンス画面を出す仕組みを作った
解決したい課題
- 複数の用途の WEB サーバ (EC2) が ELB (ALB) 配下に存在しているが、順番にメンテナンス画面を出すのに時間がかかってしまう (Jenkins などで順番にデプロイ)
- 複数の環境のソースコード上にあるメンテナンス時間を修正して、都度デプロイしないといけない環境など
- ALB に存在する リクエストルーティング を使ってメンテナンス画面を出す方法もあるが、1024 文字の制限があるので、最小限の HTML かテキストの出力しかできない
解決案
- メンテナンス用の S3 + CloudFront + route53 (ここでは maintenance.hapitas.jp と定義) をたてる
- CloudFront は
GET, HEAD, OPTIONS
以上を許可- CloudFront は
Cache Based on Selected Request Headers
をWhitelist
とし、Access-Control-Request-Headers
Access-Control-Request-Method
Origin
を Whitelist Headers として登録 (同一ドメイン、スキームでアクセスするなら不要)- S3 上では CORS 制限をはずす
- バケットの [Permissions] 設定の [CORS configuration] で下記の内容を登録 (同一ドメイン、スキームでアクセスするなら不要)
<?xml version="1.0" encoding="UTF-8"?> <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> <MaxAgeSeconds>3000</MaxAgeSeconds> <AllowedHeader>Authorization</AllowedHeader> </CORSRule> </CORSConfiguration>
- S3 バケットに
maintenance.json
ファイルを以下の内容で配置{ "start_date": "Fri, 13 Dec 2019 21:00:00 +0900", "end_date": "Fri, 13 Dec 2019 22:00:00 +0900" }
- S3 バケットにこだわったデザインの HTML (mainte.html) を配置 (適当ですがw)
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no"> <title>こだわったメンテ画面</title> </head> <body> <div> <h1>ただいまメンテナンス中です</h1> <div id="mainte-period"></div> <p>ご不便をおかけしてしまい申し訳ございませんが、<br> メンテナンス完了までしばらくお待ちください。</p> </div> <script id="template" type="text/x-jquery-tmpl"> <div id="mainte-period"> <p>実施期間: ${start} ~ ${end}<br> 期間中はすべてのサービスがご利用いただけません。</p> </div> </script> <script src="https://code.jquery.com/jquery-latest.min.js"></script> <script src="https://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js"></script> <script> "use strict"; $(function(){ var maintenance = { template: $('#template'), targetHtml: $('#mainte-period'), init: function() { this.getJson().done(function(data) { var start = false; var end = false; if (typeof data.start_date == 'string' || typeof data.end_date == 'string') { if (maintenance.isMainteNow(data.start_date, data.end_date)) { start = maintenance.generateFormat(data.start_date); end = maintenance.generateFormat(data.end_date); } } if (!start || !end) { return false; // do nothing. } maintenance.targetHtml.replaceWith(maintenance.template.tmpl({"start": start, "end": end})); }); }, isMainteNow: function(start, end) { if (!start || !end) { return false; } var now = new Date(); var start = new Date(start); var end = new Date(end); return start.getTime() <= now.getTime() && now.getTime() <= end.getTime(); }, generateFormat: function(string) { var dateObject = new Date(string); var year = dateObject.getFullYear(); var month = dateObject.getMonth() + 1; var date = dateObject.getDate(); var hours = dateObject.getHours(); var minutes = dateObject.getMinutes(); var week = [ "日", "月", "火", "水", "木", "金", "土" ][dateObject.getDay()]; return year + '年' + month + '月' + date + '日 ' + ' (' + week + ') ' + ('0' + hours).slice(-2) + ':' + ('0' + minutes).slice(-2); }, getJson: function() { var deferred = $.Deferred(); var date = new Date(); $.ajax({ type: 'GET', url: 'https://maintenance.hapitas.jp/maintenance.json?' + date.getTime(), dataType: 'json', success: deferred.resolve, error: function(xhr, textStatus, errorThrown) { console.log(xhr, textStatus, errorThrown); deferred.reject(xhr, textStatus, errorThrown); } }); return deferred.promise(); } }; maintenance.init(); }); </script> </body> </html>
- ALB の リクエストルーティングの優先度は default action の手前の優先度で ルールを追加 (※ default action より優先度は下げられないため、手前に配置する。ただし、非メンテ時は default action に重要なアクションがあると困る → derault action と同じ内容のルールを Source IP 0.0.0.0/0 でメンテナンスのルールより手前に追加しておく)
- IF に Source IP を 0.0.0.0/0 とする
- THEN に Return fixed response をいれ Response code: 503, Content-Type: test/html, Response body に以下をいれる (なんと 301 文字)
<!DOCTYPE html><html lang="ja"><head><meta charset="utf-8"></head><body><script onload="$.ajax({type: 'GET',url: 'https://maintenance.hapitas.jp/mainte.html',dataType: 'html',success: function(data) {document.write(data);}});" src="https://code.jquery.com/jquery-latest.min.js"></script></body></html>これで準備は完了!
メンテ時は json のメンテ開始日、終了日をいれ、上記レスポンスの優先度を一番上に上げるだけ
緊急メンテで、開始日、終了日が決まっていない場合は、 json の更新はせず、優先度を上げればOK
- 投稿日:2019-12-13T21:26:52+09:00
【ALB】さくっと5秒でメンテナンス画面を出す仕組みを作った
解決したい課題
- 複数の用途の WEB サーバ (EC2) が ELB (ALB) 配下に存在しているが、順番にメンテナンス画面を出すのに時間がかかってしまう (Jenkins などで順番にデプロイ)
- 複数の環境のソースコード上にあるメンテナンス時間を修正して、都度デプロイしないといけない環境など
- ALB に存在する リクエストルーティング を使ってメンテナンス画面を出す方法もあるが、1024 文字の制限があるので、最小限の HTML かテキストの出力しかできない
解決案
- メンテナンス用の S3 + CloudFront + route53 (ここでは maintenance.hapitas.jp と定義) をたてる
- CloudFront は
GET, HEAD, OPTIONS
以上を許可- CloudFront は
Cache Based on Selected Request Headers
をWhitelist
とし、Access-Control-Request-Headers
Access-Control-Request-Method
Origin
を Whitelist Headers として登録 (同一ドメイン、スキームでアクセスするなら不要)- S3 上では CORS 制限をはずす
- バケットの [Permissions] 設定の [CORS configuration] で下記の内容を登録 (同一ドメイン、スキームでアクセスするなら不要)
<?xml version="1.0" encoding="UTF-8"?> <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <CORSRule> <AllowedOrigin>*</AllowedOrigin> <AllowedMethod>GET</AllowedMethod> <MaxAgeSeconds>3000</MaxAgeSeconds> <AllowedHeader>Authorization</AllowedHeader> </CORSRule> </CORSConfiguration>
- S3 バケットに
maintenance.json
ファイルを以下の内容で配置{ "start_date": "Fri, 13 Dec 2019 21:00:00 +0900", "end_date": "Fri, 13 Dec 2019 22:00:00 +0900" }
- S3 バケットにこだわったデザインの HTML (mainte.html) を配置 (適当ですがw)
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no"> <title>こだわったメンテ画面</title> </head> <body> <div> <h1>ただいまメンテナンス中です</h1> <div id="mainte-period"></div> <p>ご不便をおかけしてしまい申し訳ございませんが、<br> メンテナンス完了までしばらくお待ちください。</p> </div> <script id="template" type="text/x-jquery-tmpl"> <div id="mainte-period"> <p>実施期間: ${start} ~ ${end}<br> 期間中はすべてのサービスがご利用いただけません。</p> </div> </script> <script src="https://code.jquery.com/jquery-latest.min.js"></script> <script src="https://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.min.js"></script> <script> "use strict"; $(function(){ var maintenance = { template: $('#template'), targetHtml: $('#mainte-period'), init: function() { this.getJson().done(function(data) { var start = false; var end = false; if (typeof data.start_date == 'string' || typeof data.end_date == 'string') { if (maintenance.isMainteNow(data.start_date, data.end_date)) { start = maintenance.generateFormat(data.start_date); end = maintenance.generateFormat(data.end_date); } } if (!start || !end) { return false; // do nothing. } maintenance.targetHtml.replaceWith(maintenance.template.tmpl({"start": start, "end": end})); }); }, isMainteNow: function(start, end) { if (!start || !end) { return false; } var now = new Date(); var start = new Date(start); var end = new Date(end); return start.getTime() <= now.getTime() && now.getTime() <= end.getTime(); }, generateFormat: function(string) { var dateObject = new Date(string); var year = dateObject.getFullYear(); var month = dateObject.getMonth() + 1; var date = dateObject.getDate(); var hours = dateObject.getHours(); var minutes = dateObject.getMinutes(); var week = [ "日", "月", "火", "水", "木", "金", "土" ][dateObject.getDay()]; return year + '年' + month + '月' + date + '日 ' + ' (' + week + ') ' + ('0' + hours).slice(-2) + ':' + ('0' + minutes).slice(-2); }, getJson: function() { var deferred = $.Deferred(); var date = new Date(); $.ajax({ type: 'GET', url: 'https://maintenance.hapitas.jp/maintenance.json?' + date.getTime(), dataType: 'json', success: deferred.resolve, error: function(xhr, textStatus, errorThrown) { console.log(xhr, textStatus, errorThrown); deferred.reject(xhr, textStatus, errorThrown); } }); return deferred.promise(); } }; maintenance.init(); }); </script> </body> </html>
- ALB の リクエストルーティングの優先度は default action の手前の優先度で ルールを追加 (※ default action より優先度は下げられないため、手前に配置する。ただし、非メンテ時は default action に重要なアクションがあると困る → derault action と同じ内容のルールを Source IP 0.0.0.0/0 でメンテナンスのルールより手前に追加しておく)
- IF に Source IP を 0.0.0.0/0 とする
- THEN に Return fixed response をいれ Response code: 503, Content-Type: test/html, Response body に以下をいれる (なんと 301 文字)
<!DOCTYPE html><html lang="ja"><head><meta charset="utf-8"></head><body><script onload="$.ajax({type: 'GET',url: 'https://maintenance.hapitas.jp/mainte.html',dataType: 'html',success: function(data) {document.write(data);}});" src="https://code.jquery.com/jquery-latest.min.js"></script></body></html>これで準備は完了!
メンテ時は json のメンテ開始日、終了日をいれ、上記レスポンスの優先度を一番上に上げるだけ
緊急メンテで、開始日、終了日が決まっていない場合は、 json の更新はせず、優先度を上げればOK
- 投稿日:2019-12-13T20:58:15+09:00
Twitter のツリーのテキストを Chrome のコンソールで取得しよう
Twitter の連続ツイートのテキストをプレーンなテキストでメモしたくなった時に、コピーが難しかったので作りました。
⚠注意点⚠
法と良識の範囲でのご利用をお願いします。
スクリプト全体
{ const targetDom = document.querySelectorAll('div[lang="ja"][dir="auto"]'); const checkLF = () => { if(navigator.platform.indexOf("Win") != -1) { return "\r\n"; }else{ return "\n"; } }; const logText = () => { if (!targetDom) { alert(`ツイートの取得に失敗しました`); return false; } const maxTweets = 30; const resultAlertText = `処理が完了しました。ログをご確認ください。`; let resultText; let lineFeedCode = checkLF(); for (let i = 0; i < targetDom.length; i++) { const tmpText = targetDom[i].textContent; resultText = resultText + lineFeedCode + tmpText; if(maxTweets < i){ resultAlertText = `処理が完了しました。ログをご確認ください。${maxTweets}ぐらい以降のツイートは取得していません。`; break; } } console.log(resultText); alert(resultAlertText); }; logText(); }使い方
- Twitter の Web 版の画面を Chrome で開いてください。
- console にコード全体をコピペして Enter キーを押してください。
- コンソールにテキストが表示されます。適当にコピーしてご利用ください。
Q & A
連続ツイートのテキストの一部しか取得できない。
環境によって違うようですが、一度に取得できるツイート数に上限があるようです。
適当に表示箇所を変更して再度お試しください。日本語以外取得できない。Twitter にログインしていないと取得できない。
以下の部分を適切なセレクタに変更してご利用ください。
const targetDom = document.querySelectorAll('div[lang="ja"][dir="auto"]');UI は作らないのか?
あまり簡単に使えるようにすると、いろいろと問題がありそうなので、あえて不便にしています。
- 投稿日:2019-12-13T20:53:14+09:00
Riot.js v4 カスタムテンプレートを実装する
Riot.js Advent Calendar 2019 の13日目が空いていたので埋めます。
はじめに
前回のRiot.js v4 HTMLのテンプレートエンジンにPugを使うで、@riotjs/compilerの
registerPreprocessor
で自由にテンプレートを登録できるということがわかりました。これを検証してみようと思います。
動きを確認
まずはデフォルトのテンプレートエンジンの動きがどうなっているかを確認。
https://github.com/riot/compiler/blob/master/src/preprocessors.js/riot/compiler/blob/master/src/preprocessors.jsexport const preprocessors = Object.freeze({ javascript: new Map(), css: new Map(), template: new Map().set('default', code => ({ code })) })なるほどなるほど、引数で受け取った
code
を元になんやかんやして、{ code: "最終的なコード" }
を返せば良さそうですね。
デフォルトでは何もしないこともわかりました。動きを確認するために、適当に登録してみます。
webpack.config.jsconst path = require('path'); const compiler = require('@riotjs/compiler'); // カスタムテンプレートを登録 compiler.registerPreprocessor('template', 'step_count', (code, options) => { console.log("Hello World!!"); console.log("code:", code); console.log("options:", options); return { code: code }; }); module.exports = { mode: 'development', //mode: 'production', entry: './src/scripts/index.js', output: { path: path.resolve(__dirname, 'app/scripts'), filename: 'bundle.js', publicPath: '/scripts/', }, devtool: 'inline', //devtool: 'source-map', module: { rules: [ { test: /\.riot$/, exclude: /node_modules/, use: [{ loader: '@riotjs/webpack-loader', options: { hot: true, // set it to true if you are using hmr // add here all the other @riotjs/compiler options riot.js.org/compiler template: 'step_count' } }] }, { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] } };package.json{ "name": "riotv4-custom-template-sample", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "webpack --mode production --devtool source-map", "start": "webpack-dev-server --inline --watch --hot --colors --content-base app/ --open-page index.html --historyApiFallback true -d --port 4500" }, "keywords": [], "author": "KAJIKEN <kentaro@kajiken.jp> (http://kajiken.jp)", "license": "MIT", "dependencies": {}, "devDependencies": { "@babel/core": "^7.7.5", "@babel/preset-env": "^7.7.6", "@riotjs/compiler": "^4.5.3", "@riotjs/hot-reload": "^4.0.0", "@riotjs/webpack-loader": "^4.0.1", "babel-loader": "^8.0.6", "riot": "^4.7.1", "webpack": "^4.41.2", "webpack-cli": "^3.3.10", "webpack-dev-server": "^3.9.0" } }
npm run build
でビルド。ビルド中のログ> webpack --mode production --devtool source-map Hello World!! code: <app> <p>{ props.message }</p> </app> options: { tagName: null, fragments: null, options: { template: 'step_count', file: 'c:\\nodeapp\\riotv4-custom-template\\src\\scripts\\app.riot', scopedCss: true, hot: true }, source: '<app>\r\n <p>{ props.message }</p>\r\n</app>\r\n' } Hash: b56824acee93ca59aa1d Version: webpack 4.41.2 Time: 1403ms動いていますね!
ファイルパスもoptions
から取れるので、コミットするなどファイルを使って何かすることもできそうです。ステップカウント
とりあえずステップ数(とは名ばかりの行数)を計測してみます。
webpack.config.js// カスタムテンプレートを登録 compiler.registerPreprocessor('template', 'step_count', (code, options) => { console.log(`*** Step Count *** ${ (new String(options.options.file)).replace(/.*\\/, "") }:${ code.split("\n").length }`); return { code: code }; });ビルド中のログ> webpack --mode production --devtool source-map *** Step Count *** goodbye.riot:6 *** Step Count *** message.riot:22 *** Step Count *** hello.riot:4 *** Step Count *** app.riot:9 Hash: 6dc6caa7dcd7ec4463ef Version: webpack 4.41.2 Time: 2757msモールス信号HTMLテンプレートエンジン
webpack.config.jsconst path = require('path'); const compiler = require('@riotjs/compiler'); // モールス信号パターン const pattern = { "・-": "a", "-・・・": "b", "-・-・": "c", "-・・": "d", "・": "e", "・・-・": "f", "--・": "g", "・・・・": "h", "・・": "i", "・---": "j", "-・-": "k", "・-・・": "l", "--": "m", "-・": "n", "---": "o", "・--・": "p", "--・-": "q", "・-・": "r", "・・・": "s", "-": "t", "・・-": "u", "・・・-": "v", "・--": "w", "-・・-": "x", "-・--": "y", "--・・": "z", "・----": "1", "・・---": "2", "・・・--": "3", "・・・・-": "4", "・・・・・": "5", "-・・・・": "6", "--・・・": "7", "---・・": "8", "----・": "9", "-----": "0", "-・・・・-": "-" }; // モールス信号解読 const decode = target => target.split(" ").map(word => pattern[word] || word).join(""); // モールス信号テンプレートを登録 compiler.registerPreprocessor('template', 'morse', code => { var reg = /(<\/{0,1})(.+?)(>)/g; var match; var ret = code; while ((match = reg.exec(code)) !== null) { ret = ret.replace(match[0], `${ match[1] }${ decode(match[2]) }${ match[3] }`); } console.log(ret); return { code: ret }; }); module.exports = { mode: 'development', //mode: 'production', entry: './src/scripts/index.js', output: { path: path.resolve(__dirname, 'app/scripts'), filename: 'bundle.js', publicPath: '/scripts/', }, devtool: 'inline', //devtool: 'source-map', module: { rules: [ { test: /\.riot$/, exclude: /node_modules/, use: [{ loader: '@riotjs/webpack-loader', options: { hot: true, // set it to true if you are using hmr // add here all the other @riotjs/compiler options riot.js.org/compiler template: 'morse' } }] }, { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] } };app.riot<・- ・--・ ・--・> <・--・>{ props.message }</・--・> </・- ・--・ ・--・>hello.riot<-- -・-- -・・・・- ・・・・ ・ ・-・・ ・-・・ ---> <・・・・ ・---->Hello World!!</・・・・ ・----> </-- -・-- -・・・・- ・・・・ ・ ・-・・ ・-・・ --->goodbye.riot<-- -・-- -・・・・- --・ --- --- -・・ -・・・ -・-- ・> <・--・>Goodbye World!!</・--・> </-- -・-- -・・・・- --・ --- --- -・・ -・・・ -・-- ・>message.riot<-- -・-- -・・・・- -- ・ ・・・ ・・・ ・- --・ ・> <-・・ ・・ ・・・->{ props.message.slice(0, 1).toUpperCase() }{ props.message.slice(1) } World!!</-・・ ・・ ・・・-> </-- -・-- -・・・・- -- ・ ・・・ ・・・ ・- --・ ・>ビルド!
ビルド中のログ> webpack --mode production --devtool source-map <my-goodbye> <p>Goodbye World!!</p> </my-goodbye> <my-message> <div>{ props.message.slice(0, 1).toUpperCase() }{ props.message.slice(1) } World!!</div> </my-message> <my-hello> <h1>Hello World!!</h1> </my-hello> <app> <p>{ props.message }</p> </app> Hash: 8b904bfcedfcd87ff364 Version: webpack 4.41.2 Time: 2239msモールス信号HTMLテンプレートエンジンここに爆誕!
- 投稿日:2019-12-13T19:31:27+09:00
年末まで毎日webサイトを作り続ける大学生 〜56日目 Canvasを使って遊ぶ〜
はじめに
こんにちは!@70days_jsです。
canvas内でボールが動き続けるものを作りました。
MDNにあるゲームを参考にしています。今日は56日目。(2019/12/13)
よろしくお願いします。サイトURL
https://sin2cos21.github.io/day56.html
やったこと
丸いボールがゆっくり動いています。
真ん中にある四角に入ると、ボールの色と大きさが変わり、軌道が消えます。
スペースキーを押すとボールのスピードが上がります。html↓
<body> <div> spaceを押した回数(ペースアップ): <span id="spaceClick">0</span>回 </div> <canvas id="myCanvas" width="480" height="320"></canvas> </body>canvasを使っています。
css↓
canvas { background: rgba(255, 255, 255, 0.5); border: solid 1px black; }canvasのレイアウトを変えたぐらいです。
JavaScript↓
var canvas = document.getElementById("myCanvas"); var ctx = canvas.getContext("2d"); var ballRadius = 5; //ボールの大きさ var x = canvas.width / 2; //開始位置 X var y = canvas.height / 3; //開始位置 Y var dx = 1; var dy = -1; let color = "red"; let count = 0; let spaceClick = document.getElementById("spaceClick"); document.addEventListener("keydown", space, false); function drawBall() { ctx.beginPath(); ctx.arc(x, y, ballRadius, 0, Math.PI * 2); ctx.rect(220, 140, 50, 50); //x, y , 幅、高さ if (x > 220 && x < 270 && (y > 140 && y < 190)) { randomBallSize(); randomRGB(); ctx.clearRect(0, 0, canvas.width, canvas.height); } ctx.fillStyle = color; ctx.fill(); //実行 ctx.closePath(); } function draw() { if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) { dx = -dx; } if (y + dy > canvas.height - ballRadius || y + dy < ballRadius) { dy = -dy; } drawBall(); x += dx; y += dy; requestAnimationFrame(draw); } //setInterval(draw, 10); function space(e) { count++; spaceClick.innerHTML = count; if (e.keyCode == 32) { let fugou = Math.sign(dx); if (fugou === 1) { dx++; } else { dx--; } let fugou2 = Math.sign(dy); if (fugou2 === 1) { dy++; } else { dy--; } console.log("fugou: " + fugou); } } function randomRGB() { let r = Math.floor(Math.random() * 256); let g = Math.floor(Math.random() * 256); let b = Math.floor(Math.random() * 256); let rgb = "rgba(" + r + ", " + g + "," + b + ", .5)"; color = rgb; } function randomBallSize() { let ball = Math.floor(Math.random() * 8); ballRadius = ball + 3; } draw();drawBall()関数でボールを描いています。
中にあるif文はif (x > 220 && x < 270 && (y > 140 && y < 190)) {
真ん中の四角の位置です。
この中に入るとrandomBallSize();
randomRGB();
ctx.clearRect(0, 0, canvas.width, canvas.height);ボールのサイズを変更、色を変更、軌道を消します。
スペースキーの方はイベントリスナーで実装しています。
document.addEventListener("keydown", space, false);
変数dxとdyは速さを調整する変数で、これら絶対値を上げていくことでボールの速度を速くしています。
最後に、
requestAnimationFrame(draw);
この関数↑でアニメーションのフレームレートを自動で判断しています。
setInterval()よりこっちの方が効率よくていいですね。感想
やっぱりゲームみたいな描画系はcanvasを使うべきなんだなと思いました。
canvasだとチャートとか、他にも色々作れそうなので勉強してみようかな。。最後まで読んでいただきありがとうございます。明日も投稿しますのでよろしくお願いします。
参考
MDNさん今回もお世話になりました!ありがとうございます!
- 投稿日:2019-12-13T19:28:26+09:00
Cesium と 3D Tiles について(2)
はじめに
昨年度の FOSS4G Advent Calendar 2018 22日目で、Cesium と 3D Tiles について紹介しました。また、社内のテックリード会でも同様の内容で紹介しましたので、紹介できる内容に絞って、情報を展開したいと思います。地味にいいね!があるので参考になれば幸いです。
Cesium について
Cesium は、3D のビューワーエンジンで、Cesium js と Cesium ion で構成されています。
- Cesium JS
- Web 上の 3D マップ用の JavaScript ライブラリ
- Cesium ion
- ストリーミング用に3D データをタイリングし、配信するためのサービス
3D データは Cesium ion にてストリーミング用に最適化され、Cesium JS で可視化されます。3D データについて
3D Viewer の場合は、3D データの扱いが課題になる場合があるかと思います。
例えばドローンで撮影した点群データなどのデータを表示したい。データが大きい場合、ブラウザで表示する場合はパフォーマンスなども影響するといった問題があるかと思います。それらの課題を解決する手法として、3D データの扱いとして、Cesium では 3D Tiles、imagery、Terrain という技術を採用しています。
具体的には、以下のファイルを 3D Tiles としてサポートしています。
- glTF (.gltf, .glb)
- CityGML (.citygml, .xml, .gml)
- KML/COLLADA (.kml, .kmz)
- LASer (.las, .laz)
- COLLADA (.dae)
- Wavefront OBJ (.obj)
1. Cesium がサポートしているファイルと描画機構
2. Cesium がサポートしているサービス群
3D Tiles について
3D Tiles は、写真測量、3D Buildings、BIM/CAD、Point Clouds などの 3D 地理空間コンテンツをストリーミングおよびレンダリングするために設計されており、空間データの構造とタイルフォーマットを定義しています。
- https://github.com/AnalyticalGraphicsInc/3d-tiles
- OGC (Open Geospatial Consortium) にも採用
- OGC は、500以上の機関で、地理空間のコンテンツとサービス、センサーWebとIoT、GISデータ処理、データ共有に関する標準規格の開発と実装を行っています。
3D の地理空間データとして、地形データ、建物データ、CAD (または BIM) モデル、写真測量のモデル、ポイントクラウドなどのデータを 3D Tiles に変換します。
- 変換された 3D Tiles は、軽量でかつ最適化されているため、サイズが大きい 3D データでもストレスなく Web で配信することが可能
- 例えば、4.48 GB の LAS ファイルは、0.64 GB まで圧縮されると公式のサイトでも紹介
3D Tiles の仕様
- 3D Tile は、空間データの構造を持ったバイナリデータと JSON ファイルの2つのタイルセットで構成されています。
- タイルセットでは、メタデータとタイル オブジェクトの情報をタイルセットとして、JSON ファイルに記述されています。
- 次のいずれかの形式でレンダリングが可能なコンテンツとして配信が可能
フォーマット 用途 Batched 3D Model (b3dm) Heterogeneous 3D models.
テクスチャ化された地形や表面、3D 建物の外部と内、大規模なモデルなどInstanced 3D Model (i3dm) 3D model instances.
木、風車、ボルトPoint Cloud (pnts) 大量の点群データ Composite (cmpt) 異なる形式のタイルを1つのタイルに連結
- 個々のタイルは、Feature Table や Batch Table などの形式固有のコンポーネントを持つ binary blob.
- コンテンツは、建物や樹木を表す 3Dモデル、点群内のポイントなど、一連の機能を参照。各フィーチャには、タイルの Feature Table に格納している位置と外観のプロパティがあり、Batch Tableに保存されているアプリケーション固有のプロパティがある。
- クライアントは、実行時に各機能を選択し、可視化または分析のためにプロパティを取得することができる。
- Batched 3D Model および Instanced 3D Model 形式は、3D コンテンツの効率的な伝送のために設計されたオープンな仕様である glTF に基づいて構築されている。
- これらの形式のタイルコンテンツは、バイナリボディにジオメトリとテクスチャ情報を含む glTF が埋め込まれている。
3D Tiles の仕様:https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
glTF について
https://www.khronos.org/gltf/
- - - 3Dモデル対応 Khronos Group による Web 上の3D モデルの業界標準フォーマットである glTF に対応 glTF (The GL Transmission Format)とは、OpenGL系のAPI (WebGL, OpenGL ES, OpenGL) と親和性の高い、ランタイム用途向けのアセットフォーマット
※glTFファイル(.gltf)、バイナリのglTFファイル(.glb)の直接参照が可能3D Tiles で使用されている技術
- 3D コンテンツを表現するフォーマットとして、glTF (GL Transmission Format)、また、Google が開発した Draco というオープンソースの 3D データ圧縮ライブラリを使用。
- Cesium では、Draco を glTF に拡張して、Draco 圧縮モデルと 3D タイルセットを Cesium で扱うことが可能
![]()
以下は Cesium のブログでも紹介されていましたが、Draco 圧縮を使用したことでレンダリングが早くなっていることが分かるかと思います。
glTF 2.0 (gzipped) Draco 圧縮を使用した glTF 2.0 (gzipped) タイルサイズ:738 MB タイルサイズ:149 MB ロード時間:18.921秒 ロード時間:10.548秒 ※ 画像はデモ用のため 2 倍速になっています。
3D Tiles の表示例
- 3D Tiles のデータセット(点群データ)
- pointCloudDraco.pnts
- tileset.json
Cesiumでの 3D Tile のロードは、次のようになります。
var viewer = new Cesium.Viewer('cesiumContainer’); var tileset = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({ url : 'http://localhost:8003/tilesets/pointCloud/tileset.json’ })); viewer.zoomTo(tileset, new Cesium.HeadingPitchRange(0, -0.5, 0));CESIUM ion について
こちらの 3D Tiles のサービスを使用する場合は、CESIUM ion という Cesium が提供している 3D プラットフォームに登録して使用することができます。また、オンプレスでこれらのサービスも使用することができますが、Cesium とのビジネス上の契約が必要になるそうです。
Cesium ion の構成を以下に示します。
- asset.cesium.com
- ユーザーがアップロードしたデータを保存し、3D タイルセット、画像、地形、glTF、KML、および CZML(これらをまとめてアセットと呼びます)を提供する。
- api.cesium.com
- ion REST API。アカウントやアセット管理に使用される。3D Tiling を開始し、ジオコーディング、サーバーベースの分析、およびユーザーが 3Dマッピングアプリケーションを作成するために必要ものを提供する。
- cesium.com
- 3Dコンテンツのアップロードと管理、Cesium World Terrain などのコンテンツへのアクセス、および(最終的に)インタラクティブな地図作成と地理空間分析を実行するためのシングルページアプリケーション(SPA)
CESIUM ion の詳細については、Cesium のブログでも紹介されていますのでご参照下さい。
最後に
Cesium では、3D 地理空間コンテンツの仕様として、3D Tiles が採用されていますが、Esri では、i3s という仕様があります。こちらも 3D Tiles と同様に 3D コンテンツに特化しており、OGC にも採用されています。私が知る限り、両者を明確に比べたものは今のところないように思えます。
今後 3D コンテンツの扱いがますます重要になってくると思いますので、このあたりの技術に関してはキャッチアップしていく必要があると感じています。
- 投稿日:2019-12-13T19:06:18+09:00
スクレイピングする際の自分なりのベストプラクティス
これはPython Advent Calendar 2019の13日目の記事です。
Python要素薄めになっちゃったけど、スクレイピングの文脈でPythonよく出てくる気がするから許してください概要
- (自分なりの)スクレイピングシステム構築方法のベストプラクティス的なものがちょっとだけ定まった
- 公開していろいろ意見を聞いてみたい
- たぶんウェブデベロッパーがスクレピングやるときに知っておくとちょっとだけ幸せになるかもしれない
- 誰かの役に立てればいいなあ(希望)
構築方法の重要な点
重要な点は以下の2点
- DOMにアクセスしてデータを取得するスクレイピングする処理部分はJavaScriptを使う(とくにウェブデベロッパーの方は)
- Chrome, ChromeDrive, Seleniumとかのスクレピング環境にはDockerとかを使う
構成図は以下のような感じです。(OSとか書いてないけど、だいたいのイメージ)
Pythonの部分は任意でSeleniumを叩ければなんでもいいです。(以下のPythonはSeleniumを使うモノとして読み替えてください)JavaScriptを使ってスクレイピング処理
スクレピング関係のシステムを作るときにめんどくさいのはライブラリ選定だったりします。
個人的にPythonは好きだったりするので、昔は
PyQuery
とかBeautifulSoup
を触っていました。しかし、結構かなり面倒くさいです。
どういう風にセレクタを書けばいいのかググって実行して、ミスって、修正して、...みたいな感じなります。
最終的な結論が「Chrome Developer ToolsでJSのセレクタを書いて、欲しい情報を取得するJSを書いて、それをSeleniumから実行させる」です。(これが一番楽だと思います。)
(おそらくjQueryを触ったことがある人なら一番とっつきやすいはず)
以下のようなかんじ。
res = driver.execute_script(driver, """ return (() => { // 以下をChrome Developmer Toolsでいろいろ試して作成する let tmp = []; $(".sample-area li a").each( (idx, element) => tmp.push($(element).attr("href")) ) return tmp })() """)Chrome Developmer Toolsは以下のようなやつ。F12とか押したらでます。(Consoleタブを開くとJSがページ内で実行できます)
簡単な話、クラス情報とか丸コピして$(".class").text()
とかやるだけでデータとってこれたりします。これをライブラリの特有の関数でいろいろやろうと思うとめんどくさいのはなんとなくわかるとおもいます。
また、スクリプトがJSなのでスクレイピングライブラリに依存せずに、移植も楽です。
Chrome, ChromeDrive, Seleniumとかのスクレピング環境にはDockerとかを使う
ChromeDriverの設定とかChromeとのバージョンの相性とかいろいろやるのは人生の無駄遣いです。
Dockerとか環境をまるっともってこれるものを使いましょう。単純なHTML,CSSのみで構成されているサイトなら
curl
とかで引っ張ってきてやるのもいいと思います。(単純なウェブサイトなら←ここ重要)
サーバーサイドでレンダリングされていても、内部のJSでわりとなんかゴリゴリいじってるサイトは多いです。Python + Chrome + ChromeDrive + Seleniumの構成のサンプルを作ってみたのでスターをつけてもらうと喜びます。↓
https://github.com/redshoga/python-selenium-container実装の流れ
- スクレピングで取得したいデータ、場所を明確にする。
- Chrome Developer Toolsでその取得したいデータが取得できるスクリプトを作成。
- Seleniumで作ったスクリプトを動かしてデータを煮るなり焼くなりする。
おわり
マサカリダニゲロー>??? ??
- 投稿日:2019-12-13T18:48:22+09:00
そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。その2
この記事の続きになります
そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。
概要
JavaScriptでリアルタイム通信するゲームを作る
JavaScriptはよく触ってるけど、そういえばリアルタイムの通信/対戦ゲームってどうやって作るのってなった時に参考になるかもしれないです。
PlayCanvasでマルチ対戦のゲームを作るために今回はPhotonというネットワークエンジンを使用して作成します。役割としては描画の部分はPlayCanvas
、リアルタイム通信の部分についてはPhoton
を使用して作っていきます。今回デモとして作成したこちらのゲームは[WASD]で移動ができるシンプルなゲームとなっております。
2窓などをしていただければ一人でも遊べます。PhotonとPlayCanvas
— はが (@Mxcn3) December 13, 2019
[W A S D]で移動(スマホはタッチで飛ぶだけ)#playcanvas #2窓https://t.co/mPDM21LfmePlayCanvasについて
PlayCanvasはJavaScriptで記述されたWebGLのエンジンで、最近GitHubのスター数が5000を超えました。
OSSのEngineと、SaaS型のエディターを兼ね備えているゲームエンジンです。engineについては、Three.js, Babylon.jsなどに似た使い方を使用することができます。エディターはビジュアルエディターとコードエディターがあり、エディターはクラウド上でプロジェクトの作成から公開までが出来るもので、FBX
やOBJ
といった3Dモデルの素材については、ブラウザへドラッグアンドドロップをすることでGUIから操作・配置ができるエディターとなっております。Photonについて
Photonについては、Unity多く使われているようですが、JavaScriptのSDKもありますので、Photon JavaScript SDKを使用してマルチ対戦のゲームを作っていきます。
Photonは20CCU(同時接続)までであれば、無料で使用できます。
https://www.photonengine.com/ja/photon
PlayCanvasでリアルタイム通信をするをプロジェクト をこのような形で作ってみました。
init.js
起動Photonの起動を行っています。
index.html
,style.css
を読み込みUIのセットアップも行います。app.js
app.jsにてPhotonとPlayCanvasの同期をしています。Photonには様々なライフサイクルで発行されるイベントがありますが、今回使用したものについて説明します。
/*jshint esversion: 6, asi: true, laxbreak: true*/ const App = pc.createScript('app'); App.prototype.initialize = function() { this.photon = this.app.root.children[0].photon; this.photon.setLogLevel(999); this.photon.onJoinRoom = () => { Object.values(this.photon.actors).map(event => { const { isLocal, actorNr } = event; if (isLocal) { } else { const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); entity.setPosition(0, 1, 0); entity.tags.add(`${actorNr}`); this.app.root.addChild(entity); } }); }; this.photon.onActorJoin = event => { const { isLocal, actorNr } = event; if (isLocal) { } else { const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); entity.setPosition(0, 1, 0); entity.tags.add(`${actorNr}`); this.app.root.addChild(entity); } }; this.photon.onEvent = (code, content, actorNr) => { const entities = this.app.root.findByTag(`${actorNr}`); const [entity] = entities; switch (code) { case 1: { const { x, y, z } = content; entity.setLocalPosition(x, y, z); break; } case 2: { const { x, y, z, w } = content; entity.setLocalRotation(x, y, z, w); break; } default: { break; } } }; };photon.onJoinRoom
onJoinRoomはルームに入った際の処理になります。今回のような作りでは呼ばれるタイミング
- ルームを新しく作ったとき
- 既存のルームに入る時
- ルームに入った時
- 他のプレイヤーを取得
- 新しいエンティティ("Model:Box")としてゲーム画面に配置
// 1.ルームに入った時 this.photon.onJoinRoom = () => { //2. 他のプレイヤーを取得 Object.values(this.photon.actors).map(event => { const { isLocal, actorNr } = event; if (isLocal) { } else { // 新しいエンティティを作成 const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); // 新しく生成したエンティティのポジションの設定とタグを付与している entity.setPosition(0, 1, 0); // x, y, z entity.tags.add(`${actorNr}`); // PlayCanvasの画面上に配置する //3. 新しいエンティティ("Model:Box")としてゲーム画面に配置 this.app.root.addChild(entity); } }); };photon.onActorJoin
onActorJoinは自分の入っているルームに他のプレイヤーが参加してきたときの処理になります。
- 他のプレイヤーが入ってきた時
- 新しいエンティティを作成
- 新しいエンティティ("Model:Box")としてゲーム画面に配置
// 1.他のプレイヤーが入ってきた時 this.photon.onActorJoin = event => { // isLocal: 自分であるかの判定 // actorNr: 入った順に1から振られる、ユニークな番号 const { isLocal, actorNr } = event; // 自分自身であるかの判定 if (isLocal) { } else { // 2. 新しいエンティティを作成 const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); // ポジションとタグを設定 entity.setPosition(0, 1, 0); entity.tags.add(`${actorNr}`); // 3. 新しいエンティティ("Model:Box")としてゲーム画面に配置 this.app.root.addChild(entity); } };photon.onEvent
onEventは、今回は
player.js
により、データが送信された場合に呼び出されます。
actorNr
のタグを元にエンティティ
を検索Code
によってPosition
かRotation
かの判定を行うthis.photon.onEvent = (code, content, actorNr) => { // 1. `actorNr`のタグを元に`エンティティ`を検索 const entities = this.app.root.findByTag(`${actorNr}`); const [entity] = entities; //2. `Code`によって`Position`か`Rotation`かの判定を行う switch (code) { case 1: { const { x, y, z } = content; entity.setLocalPosition(x, y, z); break; } case 2: { const { x, y, z, w } = content; entity.setLocalRotation(x, y, z, w); break; } default: { break; } } };player.js
Player.jsはキーボードの操作が押された際にPhotonサーバーにデータを送るものになります。
/*jshint esversion: 6, asi: true, laxbreak: true*/ const Player = pc.createScript("player"); // PlayCanvas Editor上で使用するためにAttributesを作成 // Attributesの説明 // https://developer.playcanvas.com/ja/user-manual/scripting/script-attributes/ Player.attributes.add("moveSpeed", { type: "number", default: 0.1 }); Player.attributes.add("rotateSpeed", { type: "number", default: 2 }); const move = (direction, entity, self) => { const { photon } = self; switch (direction) { case "up": { entity.translateLocal(0, 0, -self.moveSpeed); break; } case "down": { entity.translateLocal(0, 0, self.moveSpeed); break; } default: { break; } } // send position // コード番号 1で現在のEntityのポジションを送信 photon.raiseEvent(1, entity.getLocalPosition()); }; const rotate = (direction, entity, self) => { const { photon } = self; switch (direction) { case "left": { entity.rotate(0, -self.rotateSpeed, 0); break; } case "right": { entity.rotate(0, self.rotateSpeed, 0); break; } default: { break; } } // send rotation // コード番号 2で現在のEntityのポジションを送信 photon.raiseEvent(2, entity.getLocalRotation()); }; Player.prototype.initialize = function () { this.photon = this.app.root.children[0].photon; if (this.app.touch) { this.app.touch.on( pc.EVENT_TOUCHSTART, () => { const { photon } = this const { x, y, z } = this.entity.getPosition(); this.entity.rigidbody.teleport(x, y + 0.5, z); photon.raiseEvent(1, this.entity.getLocalPosition()); photon.raiseEvent(2, this.entity.getLocalRotation()); }, null ); } }; // update code called every frame Player.prototype.update = function (dt) { const { keyboard } = this.app; // 移動のキーが押されていたら if (keyboard.isPressed(pc.KEY_W) || keyboard.isPressed(pc.KEY_UP)) { move("up", this.entity, this); } else if (keyboard.isPressed(pc.KEY_S) || keyboard.isPressed(pc.KEY_DOWN)) { move("down", this.entity, this); } // 回転のキーが押されたら if (keyboard.isPressed(pc.KEY_A) || keyboard.isPressed(pc.KEY_LEFT)) { rotate("right", this.entity, this); } else if (keyboard.isPressed(pc.KEY_D) || keyboard.isPressed(pc.KEY_RIGHT)) { rotate("left", this.entity, this); } };photon.raiseEvent
raiseEventを使用して任意のタイミングでデータの送信を行います。今回は移動が行われた時と回転が行われた際に現在の場所を送信しています。
raiseEvent(eventCode, data, options)型はそれぞれ
eventCode:number object: object options: object移動時
.... // send position // コード番号 1で現在のEntityのポジションを送信 photon.raiseEvent(1, entity.getLocalPosition()); };回転時
... // send rotation // コード番号 2で現在のEntityのポジションを送信 photon.raiseEvent(2, entity.getLocalRotation()); };参考
initialize
PlayCanvasのスクリプトのライフサイクルには、スクリプト一度だけ呼び出される
initialize
、毎フレーム呼び出されるUpdate
があります。
- Photonのプロパティを代入
- タッチされた際に発火するイベントの定義
Player.prototype.initialize = function () { // Rootエンティティからphotonプロパティを取得 // 1. Photonのプロパティを代入 this.photon = this.app.root.children[0].photon; // タッチ操作が可能だったら if (this.app.touch) { // タッチ操作が行われたら // PlayCanvas: タッチについて // https://support.playcanvas.jp/hc/ja/articles/227190908 // 2. タッチされた際に発火するイベントの定義 // PlayCanvas: イベント種類について // https://developer.playcanvas.com/ja/user-manual/scripting/communication/ this.app.touch.on( pc.EVENT_TOUCHSTART, () => { const { photon } = this const { x, y, z } = this.entity.getPosition(); this.entity.rigidbody.teleport(x, y + 0.5, z); // コード1で現在のポジションを送信 photon.raiseEvent(1, this.entity.getLocalPosition()); // コード2で現在の回転を送信 photon.raiseEvent(2, this.entity.getLocalRotation()); }, null ); } };update
update
はフレームごとに呼ばれます
- 移動のキーが押されていたら
- 回転のキーが押されたら
Player.prototype.update = function (dt) { const { keyboard } = this.app; // 1. 移動のキーが押されていたら if (keyboard.isPressed(pc.KEY_W) || keyboard.isPressed(pc.KEY_UP)) { // 移動 move("up", this.entity, this); } else if (keyboard.isPressed(pc.KEY_S) || keyboard.isPressed(pc.KEY_DOWN)) { // 移動 move("down", this.entity, this); } // 2. 回転のキーが押されたら if (keyboard.isPressed(pc.KEY_A) || keyboard.isPressed(pc.KEY_LEFT)) { // 回転 rotate("right", this.entity, this); } else if (keyboard.isPressed(pc.KEY_D) || keyboard.isPressed(pc.KEY_RIGHT)) { // 回転 rotate("left", this.entity, this); } };前回
そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。以下PlayCanvas開発で参考になりそうな記事の一覧です。
- PlayCanvas入門- モデルの作成~ゲームに入れ込むまで
- JavaScriptでスロットを実装する。【PlayCanvas】
- 3Dモデルのビューワーを3分で作る【初めてのPlayCanvas】
- PlayCanvasのコードエディターでes6に対応する
- Gulpのプラグインを書いたらPlayCanvasでの開発がめちゃくちゃ便利になった
- PlayCanvas Editorに外部スクリプトを読み込む新機能が追加されたので開発方法を考える。- Reduxを組み込む
その他の記事はこちらになります。
- AR年賀状を作る
- React Native + PlayCanvasを使ってスマートフォンゲームを爆速で生み出す
- PlayCanvasのエディター上でHTML, CSSを組み込む方法
- 【iOS13】新しくなったWebVRの使い方PlayCanvasのユーザー会のSlackを作りました!
少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください!
- 投稿日:2019-12-13T18:48:22+09:00
Photon JavaScript SDKのざっくりとした説明と、PlayCanvasでリアルタイム通信をするゲームを作る。
この記事の続きになります
そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。
概要
JavaScriptでリアルタイム通信するゲームを作る
JavaScriptはよく触ってるけど、そういえばリアルタイムの通信/対戦ゲームってどうやって作るのってなった時に参考になるかもしれないです。
PlayCanvasでマルチ対戦のゲームを作るために今回はPhotonというネットワークエンジンを使用して作成します。役割としては描画の部分はPlayCanvas
、リアルタイム通信の部分についてはPhoton
を使用して作っていきます。今回デモとして作成したこちらのゲームは[WASD]で移動ができるシンプルなゲームとなっております。
2窓などをしていただければ一人でも遊べます。PhotonとPlayCanvas
— はが (@Mxcn3) December 13, 2019
[W A S D]で移動(スマホはタッチで飛ぶだけ)#playcanvas #2窓https://t.co/mPDM21LfmePlayCanvasについて
PlayCanvasはJavaScriptで記述されたWebGLのエンジンで、最近GitHubのスター数が5000を超えました。
OSSのEngineと、SaaS型のエディターを兼ね備えているゲームエンジンです。engineについては、Three.js, Babylon.jsなどに似た使い方を使用することができます。エディターはビジュアルエディターとコードエディターがあり、エディターはクラウド上でプロジェクトの作成から公開までが出来るもので、FBX
やOBJ
といった3Dモデルの素材については、ブラウザへドラッグアンドドロップをすることでGUIから操作・配置ができるエディターとなっております。Photonについて
Photonについては、Unity多く使われているようですが、JavaScriptのSDKもありますので、Photon JavaScript SDKを使用してマルチ対戦のゲームを作っていきます。
Photonは20CCU(同時接続)までであれば、無料で使用できます。
https://www.photonengine.com/ja/photon
PlayCanvasでリアルタイム通信をするをプロジェクト を作ってみました。
Photon SDKのドキュメント
PlayCanvasのプロジェクト
PlayCanvasはクラウド上でスクリプトを管理します。
init.js
起動Photonの起動を行っています。
index.html
,style.css
を読み込みUIのセットアップも行います。app.js
app.jsにてPhotonとPlayCanvasの同期をしています。Photonには様々なライフサイクルで発行されるイベントがありますが、今回使用したものについて説明します。
/*jshint esversion: 6, asi: true, laxbreak: true*/ const App = pc.createScript('app'); App.prototype.initialize = function() { this.photon = this.app.root.children[0].photon; this.photon.setLogLevel(999); this.photon.onJoinRoom = () => { Object.values(this.photon.actors).map(event => { const { isLocal, actorNr } = event; if (isLocal) { } else { const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); entity.setPosition(0, 1, 0); entity.tags.add(`${actorNr}`); this.app.root.addChild(entity); } }); }; this.photon.onActorJoin = event => { const { isLocal, actorNr } = event; if (isLocal) { } else { const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); entity.setPosition(0, 1, 0); entity.tags.add(`${actorNr}`); this.app.root.addChild(entity); } }; this.photon.onEvent = (code, content, actorNr) => { const entities = this.app.root.findByTag(`${actorNr}`); const [entity] = entities; switch (code) { case 1: { const { x, y, z } = content; entity.setLocalPosition(x, y, z); break; } case 2: { const { x, y, z, w } = content; entity.setLocalRotation(x, y, z, w); break; } default: { break; } } }; };photon.onJoinRoom
onJoinRoomはルームに入った際の処理になります。今回のような作りでは呼ばれるタイミング
- ルームを新しく作ったとき
- 既存のルームに入る時
- ルームに入った時
- 他のプレイヤーを取得
- 新しいエンティティ("Model:Box")としてゲーム画面に配置
// 1.ルームに入った時 this.photon.onJoinRoom = () => { //2. 他のプレイヤーを取得 Object.values(this.photon.actors).map(event => { const { isLocal, actorNr } = event; if (isLocal) { } else { // 新しいエンティティを作成 const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); // 新しく生成したエンティティのポジションの設定とタグを付与している entity.setPosition(0, 1, 0); // x, y, z entity.tags.add(`${actorNr}`); // PlayCanvasの画面上に配置する //3. 新しいエンティティ("Model:Box")としてゲーム画面に配置 this.app.root.addChild(entity); } }); };photon.onActorJoin
onActorJoinは自分の入っているルームに他のプレイヤーが参加してきたときの処理になります。
- 他のプレイヤーが入ってきた時
- 新しいエンティティを作成
- 新しいエンティティ("Model:Box")としてゲーム画面に配置
// 1.他のプレイヤーが入ってきた時 this.photon.onActorJoin = event => { // isLocal: 自分であるかの判定 // actorNr: 入った順に1から振られる、ユニークな番号 const { isLocal, actorNr } = event; // 自分自身であるかの判定 if (isLocal) { } else { // 2. 新しいエンティティを作成 const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); // ポジションとタグを設定 entity.setPosition(0, 1, 0); entity.tags.add(`${actorNr}`); // 3. 新しいエンティティ("Model:Box")としてゲーム画面に配置 this.app.root.addChild(entity); } };photon.onEvent
onEventは、今回は
player.js
により、データが送信された場合に呼び出されます。
actorNr
のタグを元にエンティティ
を検索Code
によってPosition
かRotation
かの判定を行うthis.photon.onEvent = (code, content, actorNr) => { // 1. `actorNr`のタグを元に`エンティティ`を検索 const entities = this.app.root.findByTag(`${actorNr}`); const [entity] = entities; //2. `Code`によって`Position`か`Rotation`かの判定を行う switch (code) { case 1: { const { x, y, z } = content; entity.setLocalPosition(x, y, z); break; } case 2: { const { x, y, z, w } = content; entity.setLocalRotation(x, y, z, w); break; } default: { break; } } };player.js
Player.jsはキーボードの操作が押された際にPhotonサーバーにデータを送るものになります。
/*jshint esversion: 6, asi: true, laxbreak: true*/ const Player = pc.createScript("player"); // PlayCanvas Editor上で使用するためにAttributesを作成 // Attributesの説明 // https://developer.playcanvas.com/ja/user-manual/scripting/script-attributes/ Player.attributes.add("moveSpeed", { type: "number", default: 0.1 }); Player.attributes.add("rotateSpeed", { type: "number", default: 2 }); const move = (direction, entity, self) => { const { photon } = self; switch (direction) { case "up": { entity.translateLocal(0, 0, -self.moveSpeed); break; } case "down": { entity.translateLocal(0, 0, self.moveSpeed); break; } default: { break; } } // send position // コード番号 1で現在のEntityのポジションを送信 photon.raiseEvent(1, entity.getLocalPosition()); }; const rotate = (direction, entity, self) => { const { photon } = self; switch (direction) { case "left": { entity.rotate(0, -self.rotateSpeed, 0); break; } case "right": { entity.rotate(0, self.rotateSpeed, 0); break; } default: { break; } } // send rotation // コード番号 2で現在のEntityのポジションを送信 photon.raiseEvent(2, entity.getLocalRotation()); }; Player.prototype.initialize = function () { this.photon = this.app.root.children[0].photon; if (this.app.touch) { this.app.touch.on( pc.EVENT_TOUCHSTART, () => { const { photon } = this const { x, y, z } = this.entity.getPosition(); this.entity.rigidbody.teleport(x, y + 0.5, z); photon.raiseEvent(1, this.entity.getLocalPosition()); photon.raiseEvent(2, this.entity.getLocalRotation()); }, null ); } }; // update code called every frame Player.prototype.update = function (dt) { const { keyboard } = this.app; // 移動のキーが押されていたら if (keyboard.isPressed(pc.KEY_W) || keyboard.isPressed(pc.KEY_UP)) { move("up", this.entity, this); } else if (keyboard.isPressed(pc.KEY_S) || keyboard.isPressed(pc.KEY_DOWN)) { move("down", this.entity, this); } // 回転のキーが押されたら if (keyboard.isPressed(pc.KEY_A) || keyboard.isPressed(pc.KEY_LEFT)) { rotate("right", this.entity, this); } else if (keyboard.isPressed(pc.KEY_D) || keyboard.isPressed(pc.KEY_RIGHT)) { rotate("left", this.entity, this); } };photon.raiseEvent
raiseEventを使用して任意のタイミングでデータの送信を行います。今回は移動が行われた時と回転が行われた際に現在の場所を送信しています。
raiseEvent(eventCode, data, options)型はそれぞれ
eventCode:number object: object options: object移動時
.... // send position // コード番号 1で現在のEntityのポジションを送信 photon.raiseEvent(1, entity.getLocalPosition()); };回転時
... // send rotation // コード番号 2で現在のEntityのポジションを送信 photon.raiseEvent(2, entity.getLocalRotation()); };参考
initialize
PlayCanvasのスクリプトのライフサイクルには、スクリプト一度だけ呼び出される
initialize
、毎フレーム呼び出されるUpdate
があります。
- Photonのプロパティを代入
- タッチされた際に発火するイベントの定義
Player.prototype.initialize = function () { // Rootエンティティからphotonプロパティを取得 // 1. Photonのプロパティを代入 this.photon = this.app.root.children[0].photon; // タッチ操作が可能だったら if (this.app.touch) { // タッチ操作が行われたら // PlayCanvas: タッチについて // https://support.playcanvas.jp/hc/ja/articles/227190908 // 2. タッチされた際に発火するイベントの定義 // PlayCanvas: イベント種類について // https://developer.playcanvas.com/ja/user-manual/scripting/communication/ this.app.touch.on( pc.EVENT_TOUCHSTART, () => { const { photon } = this const { x, y, z } = this.entity.getPosition(); this.entity.rigidbody.teleport(x, y + 0.5, z); // コード1で現在のポジションを送信 photon.raiseEvent(1, this.entity.getLocalPosition()); // コード2で現在の回転を送信 photon.raiseEvent(2, this.entity.getLocalRotation()); }, null ); } };update
update
はフレームごとに呼ばれます
- 移動のキーが押されていたら
- 回転のキーが押されたら
Player.prototype.update = function (dt) { const { keyboard } = this.app; // 1. 移動のキーが押されていたら if (keyboard.isPressed(pc.KEY_W) || keyboard.isPressed(pc.KEY_UP)) { // 移動 move("up", this.entity, this); } else if (keyboard.isPressed(pc.KEY_S) || keyboard.isPressed(pc.KEY_DOWN)) { // 移動 move("down", this.entity, this); } // 2. 回転のキーが押されたら if (keyboard.isPressed(pc.KEY_A) || keyboard.isPressed(pc.KEY_LEFT)) { // 回転 rotate("right", this.entity, this); } else if (keyboard.isPressed(pc.KEY_D) || keyboard.isPressed(pc.KEY_RIGHT)) { // 回転 rotate("left", this.entity, this); } };前回
そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。以下PlayCanvas開発で参考になりそうな記事の一覧です。
- PlayCanvas入門- モデルの作成~ゲームに入れ込むまで
- JavaScriptでスロットを実装する。【PlayCanvas】
- 3Dモデルのビューワーを3分で作る【初めてのPlayCanvas】
- PlayCanvasのコードエディターでes6に対応する
- Gulpのプラグインを書いたらPlayCanvasでの開発がめちゃくちゃ便利になった
- PlayCanvas Editorに外部スクリプトを読み込む新機能が追加されたので開発方法を考える。- Reduxを組み込む
その他の記事はこちらになります。
- AR年賀状を作る
- React Native + PlayCanvasを使ってスマートフォンゲームを爆速で生み出す
- PlayCanvasのエディター上でHTML, CSSを組み込む方法
- 【iOS13】新しくなったWebVRの使い方 ### PlayCanvasのユーザー会のSlackを作りました! 少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください!
- 投稿日:2019-12-13T18:24:10+09:00
[Vue.js] v-modelのinputの処理を間引く
v-modelでの入力する際のinput処理を間引く
input時に動く処理が多くて重くなってしまったので、間引けないか考えた時のメモ
※class styleで記述しているので注意setterが動く際にlodashのdebounceを使って間引くようにする
<template> <textarea v-model="text" /> </template> <script lang="ts"> import _ from 'lodash' import { Component, Vue } from 'vue-property-decorator' @Component export default class DebounceTextArea extends Vue { public _text: string = "" public get text(): string { return this._text } // debounceの返り値がFunctionなのでmethodとして定義しておく public debounceTextSetter = _.debounce((text: string) => { this._text = text }, 1000) public set text(text: string) { // debounceで入力が終わってから値が設定されるようにする this.debounceTextSetter(text) } } </script>
- 投稿日:2019-12-13T18:22:09+09:00
Differential Serving で必要なコードは 2 割以上削れる!
Differential Serving と言う技術は実はもう2年ほど前からPhilip Walton さんの記事をきっかけに知る人ぞ知る JS のバンドルサイズを減らせるための技術なんですけど、検索しても、日本語でこのテーマについて話す記事はないので、とりあえずまとめ記事のつもりでこれを書きました、よろしくお願いします!
何のための技術ですか?
今でも IE 11 のサポートを切れないアプリは多いと思います、そう言うアプリを作っている人はほとんど IE 11 を含めた全てのブラウザーにアプリがちゃんと起動できるように ES5 までにコードをコンパイルしていると思います。
しかし、IE 11 のために追加する polyfills や ES5 でコードを書くとどうしても必要になって来る追加コードはビルドされたバンドルを重くします。
どれぐらい重くなるかと言うのはコード次第ですが大体 20%から 30%までになります。
Shubham Kanodia さんがこの記事でいくつかのライブラリーをテストしました
しかも、ユーザー全体のほぼ 90%が ES Modules に対応しています! つまり、10%のユーザーのために 90%のユーザーに送るコードの量が多くなっています。
それでもその 10%のユーザーを切れないアプリは多いと思います、なんかこう都合良く ES Modules に対応しているユーザーだけにその小さめのコードを送れるためのフロントエンドコードはないのかな…
実はあります、これからそれを紹介します。
もの凄く簡単に言うとこれです
<!-- 主にIE 11のためのJS --> <script nomodule src="/app.legacy.js"></script> <!-- 最新ブラウザーのためのJS --> <script type="module" src="/app.js"></script>コンセプト的にはアプリのバンドルを二つ用意します。
- IE11 などの legacy ブラウザー用の ES5 にコンパイルされたバンドル。
- 最新ブラウザー用の ES Modules にコンパイルされたバンドル。
ここで重要なのは
nomodule
とtype="module"
です、これらを使ってユーザーのブラウザーにどのコードをロードするかの判断を任せます。
nomodule
とtype="module"
って何ですか?
nomodule
はscript
タグの属性で ES Modules に対応しているブラウザーにこのコードを無視するように示します。
type="module"
は逆にブラウザーにコードは ES Modules で書いてあることを示します。これらを合わせたサンプルはこれです。
このコードを ES Modules に対応しているブラウザーで見るとこうなります。
見ての通り
type="module"
になっているコードだけをダウンロードして実行します。そして、こちらのサンプルを IE 11 で見るとこうなります。
見ての通り
nomodule
になっているコードだけを実行するけど両方をダウンロードしていますね…そこまで簡単には行かないんですよね
先ほどの例もそうですけど、実は Safari 10 や Firefox 58 や IE 11 や Edge 15-18 などは両方のコードをダウンロードします。
でもこれらはほとんど Wi-Fi でしか使わない PC ブラウザーだからそこまで問題にはならないと思われがちですがタチの悪いことに Safari 10 は両方をダウンロードするだけでなく両方を実行します…
John Stew さんはこちらでいくつかのテストを行ったので詳しくはこれを見てください。
Safari 10 のこの記事を書いている時点でのユーザーの割合は 1%以下ではありますができるだけ多くのユーザーに対応したいのでこれだけだとアウトなんですよね…
じゃもう手詰まり?
実は上記のブラウザーの問題を解決できる方法はあります、しかも、それを都合良くまとめてくれる方法は色々な人気のツールにはすでに入っています。
ツールは何をしていますか?
ツールはこう言ったコードを自動的に書いてくれる。
<!-- これが実際にVue CLIを使うと出て来るコードとほぼ同じものです --> <script type="module" src="https://example.com/app.js"></script> <script> !(function() { var check = document.createElement("script"); if (!("noModule" in check) && "onbeforeload" in check) { var support = false; document.addEventListener( "beforeload", function(e) { if (e.target === check) { support = true; } else if (!e.target.hasAttribute("nomodule") || !support) { return; } e.preventDefault(); }, true ); check.type = "module"; check.src = "."; document.head.appendChild(check); check.remove(); } })(); </script> <script type="text/javascript" src="https://example.com/app.legacy.js" nomodule ></script>これは利用できる範囲の JS の feature の中で Safari 10 を見つけてその場合に module と nomodule の script が両方実行されないように働きかけます。
詳しくはこちらの gistを見てください。
これはあくまで一つのやり方で、こちらではまだ legacy ブラウザーが両方ダウンロードすることになります、これから紹介するツールの中で、それすら解決するものもあるので、自分のプロジェクトに合うものもあると思います。
Vue CLI 3+の場合
Vue CLI を使っているなら話はすごい簡単です、ビルド段階でフラグを一つ追加するだけで済みます。
vue-cli-service build --modern
webpack の場合
webpack を使っているならもう少し設定をいじる必要はあります。
- この 2 つのプラグインの内から 1 つをインストールします。
次に webpack で ES5 用の設定と ES Modules 用の設定を用意します。こちらの記事の「Generate two bundles」の部分にいい例があります。
➁ でビルドしたバンドルを ➀ のプラグインに入れます
Rollup の場合
Rollup ではrollup-plugin-index-htmlを使えば簡単にできます。
こちらは性能が良く、設定次第では両方がダウンロードされてしまう問題まで解決できるようになっています。
使い方は先ほどの webpack の使い方に似ています。
Web Components でプロジェクトを作っているなら、先ほどのプラグインを内部で使っているOpen WC の設定 がおすすめです。
Web Components のプロジェクトでなくても上記の設定が参考になると思います。
最後に
Differential Serving は個人的に誰もが知っておくべき技術だと思います、これを入れるとまだ legacy ブラウザーを使っているユーザーを犠牲にせず最新ブラウザーを使っているユーザーにもっといい体験をさせられます。いわゆる「Win Win」な話です。
もっと知りたい人には(全て英語の記事ですみません mm)
- https://philipwalton.com/articles/deploying-es2015-code-in-production-today/
- https://github.com/johnstew/differential-serving
- https://www.smashingmagazine.com/2018/10/smart-bundling-legacy-code-browsers/
- https://css-tricks.com/differential-serving/
- https://dev.to/thejohnstew/differential-serving-3dkf
- 投稿日:2019-12-13T18:20:34+09:00
【Rails6】(送信時のリロード無し!)Action CableでSlack風チャットアプリを作成
Railsの学習をされた方なら誰しも一度は作ったであろうTwitterの簡易クローンアプリ。最初に実装したときは大変でした
この記事では, Action Cable を利用して, Slack のようなリアルタイムチャットアプリの作成方法,さらに Javascript でいろいろな機能を付けるところまでを解説します!
なお, Action Cable を扱う多くのチャットアプリ記事では,メッセージ送信時にリロードが発生しますが,この記事では送信も非同期で行えるように設計します。
容量制限の都合で見づらいですが,こちらが完成後のチャットアプリです。
1. Action Cableがなぜ必要なのか?
Action Cable をご存知ない方もおられると思いますので,簡単に解説したいと思います。通常のTwitterの簡易クローンアプリ(CRUDアプリ)と,この記事で作成するアプリとの大きな違いは次の2点です。
- ページ更新せず(リロードせず)にメッセージの新規投稿ができ,投稿一覧に反映される
- 他人が投稿したメッセージが,ページ更新無しに投稿一覧に反映される
1つ目は, Ajax を利用することで実装できます。ところが,2つ目は厄介です。
HTTPの仕様により,原則として,リクエストを出さなければサーバー側からデータを取得することはできません。つまり,リアルタイムで「誰かが投稿した」という情報を受け取ることはできないのです
この問題を解決し,低コストで双方向通信できるプロトコルが WebSocket であり,この WebSocket をRailsで簡単に扱える機能が Action Cable なのです!
2. 実装するアプリの仕様について
Slack のように 新規メッセージが下に来る 設計
- Twitter のように,新規メッセージが上に来る仕様よりも難易度が上がります
- 例えば,ページを開いた時に 一番下に移動させないと新規メッセージが見られない!!! という問題に対処しなければなりません
![]()
未入力時は投稿ボタンを無効化し,色を変化
入力フォームで改行した際に縦幅を広げる
無限スクロール機能(過去メッセージの読み込み機能)を実装
- 大量のメッセージを全て読み込むと双方の負荷が大きすぎるため
補足
- ログインページのデザイン部分を整える内容は省略します(以前に書きました)
- Rails だけでなく,Javascript のプログラムもそれなりに書きます
- CoffeeScript は使用しません。
- デザイン部分を楽に仕上げるため, Bootstrap4 を積極的に使用します
- jQuery は極力避けます(便利なときだけ使用します)
3. 開発環境
- macOS Catalina 10.15.1
- Ruby 2.6.4
- Rails 6.0.1
- Rails 5 の場合は,一部設定が変わります
- Bootstrap 4.3.1
- Devise 4.7.1
4. 手順
4-0. 準備
まずはアプリを作成し,すぐに必要となる gem を入れて動作確認をしておきましょう。
rails new
のオプション部分はお好みで
-d postgresql
データベースを PostgreSQL に設定-T
Minitest 用のファイル・ディレクトリを作成しない--skip-coffee
CoffeeScript のセットアップをしない--skip-bundle
bundle install をしない$ rails new chat_app -d postgresql -T --skip-coffee --skip-bundle $ cd chat_app
rails g controller
で余計なファイルを作成しないように,config/initializers
に次のgenerators.rb
を作成
- 設定はお好みで
- ただし,
g.javascripts false
にしてしまうと,rails g channel
で room_channel.js が作成されなくなるので注意config/initializers/generators.rbRails.application.config.generators do |g| g.helper false g.stylesheets false g.javascripts true g.template_engine :erb g.skip_routes true g.test_framework false end
Gemfile
に次を追加Gemfile# ログイン機能 gem 'devise' # 日本語化 gem 'rails-i18n', '~> 6.0' gem 'devise-i18n' # こちらはお好みです。ログインページにBootstrapが適用され,見た目がマシになります gem 'devise-bootstrap-views', '~> 1.0' # こちらもお好みです。動作確認用のランダムメッセージを入れるために使用します gem 'faker'$ bundle install $ rails webpacker:install # 念のため動作確認をしておきます $ rails db:create $ rails s
http://localhost:3000
で「Yay! You’re on Rails!」を確認できたら念のためコミットし,ブランチを変えておくのがよいかと思います4-1. Bootstrap4 の導入
デザイン部分を楽に仕上げるため,この記事では Bootstrap4 を積極的に使用していきます。Rails 6 の場合は次の設定を行いましょう。
$ yarn add bootstrap jquery popper.js
environment.js
を次に置き換えるconfig/webpack/environment.jsconst { environment } = require('@rails/webpacker') const webpack = require('webpack') environment.plugins.append('Provide', new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', Popper: ['popper.js', 'default'] })) module.exports = environmentapp/javascript/packs/application.js// 一番下に次を追加 require("bootstrap/dist/js/bootstrap") // js.erb内でjQueryを使用されたい場合は,「window.$ = jQuery;」も必要です
application.css
の拡張子css
をscss
に変更して,次に置き換えるapp/assets/stylesheets/application.scss/* *= require_tree . *= require_self */ @import "bootstrap/scss/bootstrap";4-2. 基本設定
誰が投稿したメッセージであるか を特定するには ログイン機能 が必要となります。そこで Devise でログイン機能を付け,ログイン関連のリンクを付けたヘッダーを付けておきます。また,ログイン後のトップページ(チャットルーム)も作成しておきましょう。
- ヘッダー(ナビバー)を追加し,レスポンシブ対応のためのmetaタグを追加
- お好みで変更して下さい
Bootstrap4
の場合は,ヘッダーのクラスにsticky-top
属性を付けるだけで位置を固定化できますapp/views/layouts/application.html.erb(略) - <title>ChatApp</title> + <title>Slack風チャットルーム</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> + <meta name="viewport" content="width=device-width,initial-scale=1"> (略) <body> + <%= render 'shared/header' %> <%= yield %> </body>app/views/shared/_header.html.erb<header class="sticky-top"> <nav class="navbar navbar-expand-sm navbar-light bg-light"> <%= link_to "Slack風チャットルーム", root_path, class: 'navbar-brand' %> <button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="ナビゲーションの切替"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav"> <% if user_signed_in? %> <li class="nav-item active"> <%= link_to 'アカウント編集', edit_user_registration_path, class: 'nav-link' %> </li> <li class="nav-item active"> <%= link_to 'ログアウト', destroy_user_session_path, method: :delete, class: 'nav-link' %> </li> <% else %> <li class="nav-item active"> <%= link_to "新規登録", new_user_registration_path, class: 'nav-link' %> </li> <li class="nav-item active"> <%= link_to "ログイン", new_user_session_path, class: 'nav-link' %> </li> <% end %> </ul> </div> </nav> </header>
- トップページ用のコントローラとビューを作成
$ rails g controller rooms show
- トップページを設定
config/routes.rbRails.application.routes.draw do root 'rooms#show' end
- 日本語化とタイムゾーンの変更
config/application.rbmodule AssociationTutorial class Application < Rails::Application # (略) # the framework and any gems in your application. # ********** 以下を追加 ********** config.i18n.default_locale = :ja config.time_zone = 'Asia/Tokyo' # ********** 以上を追加 ********** # Don't generate system test files. config.generators.system_tests = nil end end
Devise
でログイン機能を付ける$ rails g devise:install $ rails g devise user $ rails db:migrate
- 全ページをログイン必須に変更
app/controllers/application_controller.rbclass ApplicationController < ActionController::Base before_action :authenticate_user! end
$ rails s
でサーバーを再起動して確認すると,次のような状態になります
- ログインページの見た目をお好みで設定
- 以前に最低限の見た目を整える記事を書きました
- Deviseでログイン機能を追加・日本語化・Bootstrap4適用まで
4-3. メッセージ投稿機能(Ajax)
DHH氏の動画から派生した多くの記事では, Action Cable のデータ送信機能(speakなど)を利用していますが,これを使いますとメッセージ送信時にリロードが発生してしまいます
そこで,この記事ではメッセージの送信は Ajax を用いて非同期で送信することとします。これにより Action Cable 特有の知識・問題の一部を回避することもできます
まずはメッセージ投稿機能を付けます。大雑把な手順を確認しましょう。
messages
テーブルとモデルを作成- 投稿一覧ページに,
form_with
で投稿フォームを作成- コントローラの
create
アクションで投稿内容をデータベースに保存
- 投稿メッセージを非同期で投稿一覧に反映する部分はここで実装しません
メッセージを保存するためのデータベースとモデルを作成します。
user_id
は User モデルとの関連付けで必要content
はメッセージ内容を保存するカラム$ rails g model Message user_id:integer content:text
- 念のため
null: false
を追加しておきます。db/migrate/日時_create_messages.rbt.integer :user_id, null: false t.text :content, null: false$ rails db:migrate
- モデルに関連付けとバリデーションを入れておきます。
- メッセージの文字数制限を 500 文字にしていますが,お好みで
app/models/user.rbclass User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable # 次の一行を追加 has_many :messages, dependent: :destroy endapp/models/message.rbclass Message < ApplicationRecord # **********以下を追加********** belongs_to :user validates :user_id, presence: true validates :content, presence: true, length: { maximum: 500 } # **********以上を追加********** end
- コントローラ側で,(この時点では)全メッセージを取得することにします
Message.all
では,いわゆるN+1問題
が発生するので注意(後に補足します)app/controllers/rooms_controller.rbclass RoomsController < ApplicationController def show # 投稿一覧表示に利用 @messages = Message.includes(:user).order(:id) # メッセージ投稿に利用 @message = current_user.messages.build end end
- 投稿フォームを作成する前に,コントローラを作成し,ルーティングを設定
$ rails g controller messages create
- ここで作成される
app/views/messages/create.html.erb
は削除config/routes.rbRails.application.routes.draw do devise_for :users root 'rooms#show' # 次の一行を追加 resources :messages, only: :create end
- メッセージを表示するためのビューと,投稿フォームを作成
Bootstrap4
の場合は,フッターのクラスにfixed-bottom
属性を付けるだけで位置を固定化できます- Ajax を利用しますので,
form_with
にlocal: true
を入れてはいけません!app/views/rooms/show.html.erb<div id="message-container"> <%= render @messages %> </div> <%= render 'footer' %>app/views/rooms/_footer.html.erb<footer class="fixed-bottom" id="footer"> <%= form_with model: @message do |f| %> <div class="form-group"> <!-- ここは自動的に message_content というidが付きます。アンダーバーなので注意! --> <%= f.text_area :content, class: 'form-control', rows: '1', maxlength: '500' %> </div> <div class="form-group"> <%= f.submit '送信', class: 'btn btn-primary btn-block', id: 'message-button' %> </div> <% end %> </footer>
<%= render @messages %>
に挿入する部分テンプレートを作成
messages
ディレクトリの中に次の_message.html.erb
を作成- とりあえず
Eメール
,作成日時
,メッセージ
を表示することにしておきますapp/views/messages/_message.html.erb<div class="message" id="message-<%= message.id %>"> <p><%= message.user.email %>: <%= l message.created_at, format: '%Y年%-m月%-d日(%a) %H:%M' %></p> <%= simple_format(h message.content) %> </div>
補足
- 単純に
<%= message.count %>
では,改行が反映されませんsimple_format
メソッド単独では<h1>
タグなどが反映されてしまうため,h
メソッドで全てのHTMLタグをエスケープしておきます- メーセージを書いたユーザーの情報を
message.user
で取得しているので, コントローラで@messages = Message.all
にしていると各メッセージに対してSQLが発行されてしまいます(N+1問題)
application.scss
に最低限度追加しておきます
- 一番下のメッセージがフッターに隠れないよう
padding-bottom
は大きめに取っておく必要がありますapp/assets/stylesheets/application.scss// メッセージ一覧 #message-container { max-width: 768px; margin: 0 auto; padding: 1rem 1rem 8rem 1rem; } // メッセージ投稿フォーム footer { max-width: 768px; padding: 1rem; margin: 0 auto; background-color: white; }
- 動作確認のための
seeds.rb
を作成します。中身はお好みで。
- 改行が反映されるかをチェックするため,改行を含むメッセージも追加しておくとよいです
- 最初は作成するメッセージ個数を少なめにしておきます
db/seeds.rb# 作成するユーザー・メッセージの個数 user_count = 3 message_count = 3 ApplicationRecord.transaction do # テストユーザーが無ければ作成 user_count.times do |n| User.find_or_create_by!(email: "test#{n + 1}@example.com") do |user| user.password = 'password' end end # メッセージを全消去した上で,サンプルメッセージを作成。メッセージを作成したユーザーはランダムに決定する Message.destroy_all user_ids = User.ids message_list = [] message_count.times do |n| user_id = user_ids.sample line_count = rand(1..4) # Fakerで1〜4行のランダムメッセージを作成 content = Faker::Lorem.paragraphs(number: line_count).join("\n") message_list << { user_id: user_id, content: content } end Message.create!(message_list) end puts '初期データの追加が完了しました!'$ rails db:seed
例えば次のアカウントでログインできるようになります
- Eメール:
test1@example.com
- パスワード:
password
この時点で次のような表示になっていると思います
- メッセージが意味不明なのは
Faker
の仕様です![]()
- メッセージを送信できるようにコントローラを設定
app/controllers/messages_controller.rbclass MessagesController < ApplicationController def create @message = current_user.messages.create!(message_params) end private def message_params params.require(:message).permit(:content) end end
- 次の
create.js.erb
を作成app/views/messages/create.js.erb// フォームに入力した文字列を消去 document.getElementById('message_content').value = ''これでメッセージを投稿できるようになりました。フォームに例えば「おはよう」と入力して送信ボタンを押します。文字が消えた後,ページを更新(リロード)し,投稿メッセージが反映されればOKです!
なお,
create.js.erb
に投稿メッセージを表示させる操作は入れません。投稿したメッセージは本人だけでなく, チャット参加者全員 に追加しなければなりません。ここで Action Cable が登場します。
4.4 Action Cable の設定・確認
Action Cable を使用し,フロント側とサーバー側が監視し合う状態(双方向通信ができる状態)にしましょう。
$ rails g channel Room # 次の2つが出力されたらOK # create app/channels/room_channel.rb # create app/javascript/channels/room_channel.jsconfig/routes.rbRails.application.routes.draw do + mount ActionCable.server => '/cable' devise_for :users root 'rooms#show' end
- これだけで監視状態ができあがります!動作確認をしてみましょう。
- 以下は省略して 4.5 に進んでもOKです
Rails 5
の場合は異なる点が複数ありますのでご注意下さいapp/channels/room_channel.rbclass RoomChannel < ApplicationCable::Channel # サーバー側からフロント側を監視できているかを確認できたときに動くメソッド def subscribed + 5.times { puts '***test***' } end #(略)
app/javascript/channels/room_channel.jsimport consumer from "./consumer" consumer.subscriptions.create("RoomChannel", { // フロント側からサーバー側を監視できているかを確認できたときに動く関数 connected() { + console.log('test') }, // (略)
$ rails s
でサーバーを再起動し,タブを更新して確認してみて下さい。接続確認できたタイミングでメッセージが出力されるはずです。
- コンソールに
***test***
が5回出力されていればOKです
- Chromeのデベロッパーツールの
Console
タブを開き,test
と表示されていればOKですチェックができたら,追加した
5.times { puts '***test***' }
とconsole.log('test')
を削除して下さい4.5 チャット機能の実装
Action Cable を利用して チャット参加者全員 が投稿メッセージをリアルタイムで受信し,ページに反映できるように設定していきましょう!
【補足】 最初に予告しましたとおり, Action Cable の送信機能は使用しません。
app/channels/room_channel.rbclass RoomChannel < ApplicationCable::Channel def subscribed # 配信する部屋名を決定 stream_from "room_channel" end def unsubscribed end endapp/controllers/messages_controller.rbclass MessagesController < ApplicationController def create @message = current_user.messages.create!(message_params) # ********** 以下を追加 ********** # 投稿されたメッセージをチャット参加者に配信 ActionCable.server.broadcast 'room_channel', message: @message.template # ********** 以上を追加 ********** end endapp/models/message.rbclass Message < ApplicationRecord belongs_to :user validates :user_id, presence: true validates :content, presence: true, length: { maximum: 500 } # ********** 以下を追加 ********** # 投稿されたメッセージをメッセージ用の部分テンプレートでHTMLに変換 def template ApplicationController.renderer.render partial: 'messages/message', locals: { message: self } end # ********** 以上を追加 ********** end【補足】
Job
を作成している記事を多く見かけますが,少なくともHerokuにデプロイする場合は必要ないようです。app/javascript/channels/room_channel.jsimport consumer from "./consumer" // turbolinks の読み込みが終わった後にidを取得しないと,エラーが出ます。 document.addEventListener('turbolinks:load', () => { // js.erb 内で使用できるように変数を定義しておく window.messageContainer = document.getElementById('message-container') // 以下のプログラムが他のページで動作しないようにしておく if (messageContainer === null) { return } consumer.subscriptions.create("RoomChannel", { connected() { }, disconnected() { }, received(data) { // サーバー側から受け取ったHTMLを一番最後に加える messageContainer.insertAdjacentHTML('beforeend', data['message']) } }) })これで,チャット機能の基礎は完成です!
サーバーを再起動し,2つのタブで
http://localhost:3000
を開き,片方に「おはよう」と入力して送信してみて下さい。
もう片方でも「おはよう」が表示されたならOKです。
(2種類のブラウザを使い,別のユーザーでログインした状態でチェックするとなおよいです)4.6 機能の改善
さて,現状では複数の問題があります。
メッセージが多くなると,ページを開いたときに最新メッセージが見えなくなってしまう
フォームが空欄でも投稿ボタンを押すと送信できてしまう
- モデル側でバリデーションは入っているが,サーバーに無用な負荷をかけてしまう
フォームが一行では複数行のメッセージを書きづらい
- 最初から複数行の幅をとると,メッセージの見える範囲が狭くなる(スマホだと致命的)
順番に解決していきましょう!
メッセージが多くなると,ページを開いたときに最新メッセージが見えなくなってしまう
ページを開いたときにページの一番下に移動させればよい!
app/javascript/channels/room_channel.js// (略) consumer.subscriptions.create("RoomChannel", { // (略) }) // ********** 以下を追加 ********** const documentElement = document.documentElement // js.erb 内でも使用できるように変数を決定 window.messageContent = document.getElementById('message_content') // 一番下まで移動する関数。js.erb 内でも使用できるように変数を決定 window.scrollToBottom = () => { window.scroll(0, documentElement.scrollHeight) } // 最初にページ一番下へ移動させる scrollToBottom() // ********** 以上を追加 ********** }
- さらにメッセージ投稿後に,投稿した最新メッセージが見られるように一番下へ移動させます
app/views/messages/create.js.erb// フォームに入力した文字列を消去 messageContent.value = '' // 一番下へスクロール scrollToBottom()
フォームが空欄でも投稿ボタンを押すとサーバーにリクエストが出せてしまう
フォームが空欄なら投稿ボタンを無効化すればよい!
- Bootstrap4 を導入しているので,クラスに disable を追加することでボタンを無効化できます
disabled
属性の追加・削除でもよいのですが,メッセージ送信後にボタンを無効化できないので採用しません(form_with
で作成したボタンは,メッセージ送信直後に自動でdisabled
属性が追加され,送信完了後にdisabled
属性が削除されます。そのためか,create.js.erb
や,room_channel.js
のreceived
関数でdisabled
属性を追加しても無効化されてしまいます)app/views/rooms/_footer.html.erb<!-- 最初はボタンを無効化するため,クラスに disabled を追加 --> - <%= f.submit '送信', class: 'btn btn-primary btn-block', id: 'message-button' %> + <%= f.submit '送信', class: 'btn btn-primary btn-block disabled', id: 'message-button' %>
- フォームに入力されたときに,空欄でなければボタンを有効化,空欄なら無効化します
app/javascript/channels/room_channel.js// (略) // ********** 以下を追加 ********** const messageButton = document.getElementById('message-button') // 空欄でなければボタンを有効化,空欄なら無効化する関数 const button_activation = () => { if (messageContent.value === '') { messageButton.classList.add('disabled') } else { messageButton.classList.remove('disabled') } } // フォームに入力した際の動作 messageContent.addEventListener('input', () => { button_activation() }) // 送信ボタンが押された時にボタンを無効化 messageButton.addEventListener('click', () => { messageButton.classList.add('disabled') }) // ********** 以上を追加 ********** })
- さらに,ボタンが無効化されているときは灰色にします
app/assets/stylesheets/application.scss// ボタン無効化時のスタイル .btn-primary.disabled { background-color: #6c757d; border-color: #6c757d; cursor: auto; }
フォームが一行では複数行のメッセージが書きづらい
改行したときにフォームの行数を増やし,行数が減ったときはフォームの行数も減らすようにする
- フォームにある改行の個数からフォームの行数を決定
- ただし,最大行数は 10 としておきます
app/javascript/channels/room_channel.js// (略) // フォームに入力した際に動作 messageContent.addEventListener('input', () => { button_activation() // ********** 以下を追加 ********** changeLineCheck() // ********** 以上を追加 ********** }) // 送信ボタンが押された時にボタンを無効化し,フォーム行数を1に戻す messageButton.addEventListener('click', () => { messageButton.classList.add('disabled') // ********** 以下を追加 ********** changeLineCount(1) // ********** 以上を追加 ********** }) // ********** 以下を追加 ********** // フォームの最大行数を決定 const maxLineCount = 10 // 入力メッセージの行数を調べる関数 const getLineCount = () => { return (messageContent.value + '\n').match(/\r?\n/g).length; } let lineCount = getLineCount() let newLineCount const changeLineCheck = () => { // 現在の入力行数を取得(ただし,最大の行数は maxLineCount とする) newLineCount = Math.min(getLineCount(), maxLineCount) // 以前の入力行数と異なる場合は変更する if (lineCount !== newLineCount) { changeLineCount(newLineCount) } } const changeLineCount = (newLineCount) => { // フォームの行数を変更 messageContent.rows = lineCount = newLineCount } // ********** 以上を追加 ********** })app/assets/stylesheets/application.scss// ユーザーがフォームサイズを自由に変更できる機能を停止させておく #message_content { resize: none; }これで,行数が自動で変化するようになります……が,別の問題が発生します。
これは,フッターの高さを変化させたのに,
padding-bottom
を変更していないからです。さらに,変更分だけ上下にスクロールさせた方が親切です。そこで,さらにコードを加えます。app/javascript/channels/room_channel.js// (略) // ********** 以下を追加 ********** const footer = document.getElementById('footer') let footerHeight = footer.scrollHeight let newFooterHeight, footerHeightDiff // ********** 以上を追加 ********** const changeLineCount = (newLineCount) => { // フォームの行数を変更 messageContent.rows = lineCount = newLineCount // ********** 以下を追加 ********** // 新しいフッターの高さを取得し,違いを計算 newFooterHeight = footer.scrollHeight footerHeightDiff = newFooterHeight - footerHeight // 新しいフッターの高さをチャット欄の padding-bottom に反映し,スクロールさせる // 行数が増える時と減る時で操作順を変更しないとうまくいかない if (footerHeightDiff > 0) { messageContainer.style.paddingBottom = newFooterHeight + 'px' window.scrollBy(0, footerHeightDiff) } else { window.scrollBy(0, footerHeightDiff) messageContainer.style.paddingBottom = newFooterHeight + 'px' } footerHeight = newFooterHeight // ********** 以上を追加 ********** } })これで,フォーム行数の増減に対して自然な動きをするようになりました!
- この時点での最終的な
room_channel.js
はこちらです。
- 変数を書く場所などを整理する作業はお好みで
app/javascript/channels/room_channel.jsimport consumer from "./consumer" // turbolinks の読み込みが終わった後にidを取得しないと,エラーが出ます。 document.addEventListener('turbolinks:load', () => { // js.erb 内で使用できるように変数を定義しておく window.messageContainer = document.getElementById('message-container') // 以下のプログラムが他のページで動作しないようにしておく if (messageContainer === null) { return } consumer.subscriptions.create("RoomChannel", { connected() { }, disconnected() { }, received(data) { // サーバー側から受け取ったHTMLを一番最後に加える messageContainer.insertAdjacentHTML('beforeend', data['message']) } }) const documentElement = document.documentElement // js.erb 内でも使用できるように変数を決定 window.messageContent = document.getElementById('message_content') // 一番下まで移動する関数。js.erb 内でも使用できるように変数を決定 window.scrollToBottom = () => { window.scroll(0, documentElement.scrollHeight) } // 最初にページ一番下へ移動させる scrollToBottom() const messageButton = document.getElementById('message-button') // 空欄でなければボタンを有効化,空欄なら無効化する関数 const button_activation = () => { if (messageContent.value === '') { messageButton.classList.add('disabled') } else { messageButton.classList.remove('disabled') } } // フォームに入力した際の動作 messageContent.addEventListener('input', () => { button_activation() changeLineCheck() }) // 送信ボタンが押された時にボタンを無効化し,フォーム行数を1に戻す messageButton.addEventListener('click', () => { messageButton.classList.add('disabled') changeLineCount(1) }) // フォームの最大行数を決定 const maxLineCount = 10 // 入力メッセージの行数を調べる関数 const getLineCount = () => { return (messageContent.value + '\n').match(/\r?\n/g).length; } let lineCount = getLineCount() let newLineCount const changeLineCheck = () => { // 現在の入力行数を取得(ただし,最大の行数は maxLineCount とする) newLineCount = Math.min(getLineCount(), maxLineCount) // 以前の入力行数と異なる場合は変更する if (lineCount !== newLineCount) { changeLineCount(newLineCount) } } const footer = document.getElementById('footer') let footerHeight = footer.scrollHeight let newFooterHeight, footerHeightDiff const changeLineCount = (newLineCount) => { // フォームの行数を変更 messageContent.rows = lineCount = newLineCount // 新しいフッターの高さを取得し,違いを計算 newFooterHeight = footer.scrollHeight footerHeightDiff = newFooterHeight - footerHeight // 新しいフッターの高さをチャット欄の padding-bottom に反映し,スクロールさせる // 行数が増える時と減る時で操作順を変更しないとうまくいかない if (footerHeightDiff > 0) { messageContainer.style.paddingBottom = newFooterHeight + 'px' window.scrollBy(0, footerHeightDiff) } else { window.scrollBy(0, footerHeightDiff) messageContainer.style.paddingBottom = newFooterHeight + 'px' } footerHeight = newFooterHeight } })4.7 無限スクロール機能
メッセージ数が少ない間は問題無いのですが,もし1万件メッセージがあったらどうでしょう。最初に全て読み込むのは,クライアント側・サーバー側双方に負荷がかかりますし,SEOにも影響が出てくるでしょう。
そこで,最初の時点では100件だけ読み込み,一番上までスクロールしたとき更に読み込むように設計してみましょう。
非同期でメッセージをさらに読み込むためには Ajax を利用しなければなりませんが,メッセージのフォームと異なり,ボタンがありません。
form_with
やlink_to
などを使うのは不自然です。そこで,
Javascript
にAjax
を利用するためのプログラムを書きます。
- まず,メッセージを大量に追加しておきます
db/seeds.rb# 作成するユーザー・メッセージの個数 user_count = 3 - message_count = 3 + message_count = 1000$ rails db:seed
- ルーティングとコントローラーを用意し,最初に読み込むメッセージを制限します
config/routes.rb+ get '/show_additionally', to: 'rooms#show_additionally'
app/controllers/rooms_controller.rbclass RoomsController < ApplicationController def show # 次の一行を変更。最新の100件のみ取得する。 @messages = Message.includes(:user).order(:id).last(100) @message = current_user.messages.build end # ********** 以下を追加 ********** def show_additionally end # ********** 以上を追加 ********** end
room_channel.js
から Ajax を利用する
- 表示済みのメッセージの内,最も古いメッセージidを送信しておく
data-remote: true
を入れることで,show_additionally.js.erb
を作動できるapp/javascript/channels/room_channel.js// (略) // ********** 以下を追加 ********** let oldestMessageId // メッセージの追加読み込みを可否を決定する変数 window.showAdditionally = true window.addEventListener('scroll', () => { if (documentElement.scrollTop === 0 && showAdditionally) { showAdditionally = false // 表示済みのメッセージの内,最も古いidを取得 oldestMessageId = document.getElementsByClassName('message')[0].id.replace(/[^0-9]/g, '') // Ajax を利用してメッセージの追加読み込みリクエストを送る。最も古いメッセージidも送信しておく。 $.ajax({ type: 'GET', url: '/show_additionally', cache: false, data: {oldest_message_id: oldestMessageId, remote: true} }) } }, {passive: true}); // ********** 以上を追加 ********** })app/controllers/rooms_controller.rb# (略) def show_additionally # ********** 以下を追加 ********** # 追加のメッセージ50件を取得する last_id = params[:oldest_message_id].to_i - 1 @messages = Message.includes(:user).order(:id).where(id: 1..last_id).last(50) # ********** 以上を追加 ********** end endapp/views/rooms/show_additionally.js.erb// 今回は上からメッセージを追加 messageContainer.insertAdjacentHTML('afterbegin', "<%= j(render @messages) %>") // メッセージが存在するときだけ,読み込み可能とする <% if @messages.present? %> showAdditionally = true <% end %>
- メッセージが正しく追加されるかを調べるために,一旦,
message.id
も表示させるようにします
- 確認後に削除してOKです
app/views/messages/_message.html.erb<div class="message" id="message-<%= message.id %>"> <p><%= message.id %>: <%= message.user.email %>: <%= l message.created_at, format: '%Y年%-m月%-d日(%a) %H:%M' %></p> <%= simple_format(h(message.content)) %> </div>これでメッセージの追加読み込みが可能となります!……が,問題が……
メッセージが追加されたのに,スクロール位置が一番上のままなのですそこで,メッセージの追加されたタイミングでスクロールさせ,同じメッセージが見える状態にしておきます。
app/views/rooms/show_additionally.js.erb// const や let を使うと2回目以降にエラーが発生します var messageHeight = messageContainer.scrollHeight // 今回は上からメッセージを追加 messageContainer.insertAdjacentHTML('afterbegin', "<%= j(render @messages) %>") var newMessageHeight = messageContainer.scrollHeight window.scrollBy(0, newMessageHeight - messageHeight) // メッセージが存在するときだけ,読み込み可能とする <% if @messages.present? %> showAdditionally = true <% end %>これで読み込み後もメッセージの位置が変わらなくなりました
4.8 Herokuにデプロイする場合の注意点
このままHerokuにプッシュしても動作しません。
cable.yml
を次のように編集してからデプロイしてみて下さい。config/cable.ymlproduction: - adapter: redis - url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> - channel_prefix: chat_app_production + adapter: async4.9 ユーザー名とサムネイルを追加
※時間がある時に追記します……
5. 参考記事・動画
6. サンプルコード
$ git clone https://github.com/T-Tsujii/chat_app.git
- 投稿日:2019-12-13T17:20:06+09:00
React まとめ①
Reactを自分なりにまとめてみた①
初めての投稿です。
Reactの復習用メモとして残します。まず前提としてReactはJavascriptのライブラリなので、Javascriptの最低限の理解が必要というのは言うまでもないことなのですが、他の言語をやったことがある方でしたら、入りやすいのかなと思います。
Javascriptを完璧にやってからReactをやるというのも良いと思いますが、ES6の記法をさらってReactを触り始めてもいいんじゃないかなって思います。
ここで重要なのは、分かんないのはJavascriptなのかReactなのかを明確にする必要があるということです。
最初ということでJSXについての記述がほとんどになりますのでご存知のかたは②にお進み下さい。
const element = <h1>Hello, world!</h1>;この構文はJSXと呼ばれるJavascriptの拡張構文です。
ReactではこのJSXを多用します。const name = 'Josh Perez'; const element = <h1>Hello, {name}</h1>; ReactDOM.render( element, document.getElementById('root') );constでnameを宣言して、中括弧に囲んでJSX内で使用しています。
PHPで言うところの
$name = “Josh Perez”;
のようなものですが、Javascriptにはlet,const,varが存在しているので差異は理解する必要があります。const element = <div tabIndex="0"></div>;文字列リテラルを属性として指定するために引用符を使用できます。
JSXではキャメルケースのプロパティ命名規則を使用します。
キャメルケースとは、アルファベットで複合語やフレーズを表記する際、各単語や要素語の先頭の文字を大文字で表記する手法のことです。
例えばtabindexはtabIndexとなります。const title = response.potentiallyMaliciousInput; // This is safe: const element = <h1>{title}</h1>;RectDOMはJSXに埋め込まれた値をレンダリングされる前にエスケープするので、
ユーザーの入力したあらゆるコードが注入できないことが保証されています。
PHPで言うところのhtmlspecialchars()のようなものが、JSXでは担保されているということでしょうか。
- 投稿日:2019-12-13T16:05:37+09:00
rtcstats-wrapperとChart.jsでWebRTC統計情報を可視してみた
これはなに?
前回の記事
https://qiita.com/monmee/items/77310482b0a6a034a887
で紹介したrtcstats-wrapperですが、具体的にどう使えるん?みたいなところは紹介してなかったので、一例を簡単に作ってみた記事。
rtcstats-wrapperの想定される使い方として
- 例えば、アプリ内にwebrtc-internal的な統計グラフを表示、ユーザに通信状況をFBする
- 例えば、パケットロス率が異常値に達したらSlackに通知するなどのWebRTC通信の監視体制を作る
- 例えば、取得した統計情報とユーザIDを紐付けて、各ユーザの通信環境パフォーマンスの解析材料とする
などがあります。今回は一番上の
例えば、アプリ内にwebrtc-internal的な統計グラフを表示、ユーザに通信状況をFBする
をやってみました。
できたもの
Gistにソースコードをupしています。
とりあえず動かしたれ!精神でやってる気合のコードでお見苦しいところもあるかと思いますが参考程度に見てください。index.html
sciprt.js
config.js左がSafari、右がFirefoxです。定期的にstatsを取得してグラフをリアタイで動かしてます。
画像では受信/送信トラフィックのみ表示してますが、rtcstats-wrapperから取得できる値であれば他の統計情報も取れます。(本当に取れるかは各ブラウザのgetStats()
実装状況によるけど)ミニ
chrome://webrtc-internals
が他ブラウザでも見れるという感じです。必要なもの
rtcstats-wrapper
各ブラウザの
getStats()
を標準化して統計情報の瞬間値を計測してくれるラッパー。WebRTCアプリ
rtcstats-wrapperは現在ブラウザP2Pのみ対応です。
SkyWayのサンプルアプリを使いました。データ可視化ライブラリ
今回使った外部ライブラリはこれ。
- Chart.js
- chartjs-plugin-streaming
- moment.js時系列データをリアルタイムにブラウザに出力できれば何でも良いと思います。
例えば、グラフ描画系ならChart.js以外にも、d3.js、Google Chartらへんが候補としてありそうです。
今回はリアルタイムデータ向けChart.jsプラグインのchartjs-plugin-streamingをChart.jsと併用しています。時間軸がスムーズに動かなかったりDeprecated Warningがコンソール表示されるあたり、chartjs-plugin-streamingはChart.js v2.8.0以降に対応してなさそうなので、今回はChart.js v2.7.3を使用しました。
Chart.jsで時系列グラフを作るためにmoment.jsも必要らしいのでそちらも必要。
前準備
使用するライブラリをダウンロードします。
Vanilla JSでimport/export文を使えるように最後の2ファイルにはtype="module"
を追加しています。
config.jsについては後述します。index.html<script src="/node_modules/rtcstats-wrapper/dist/rtcstats-wrapper.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@2.7.3/dist/Chart.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@latest/dist/chartjs-plugin-streaming.min.js"></script> <script src="//cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script> <script src="../_shared/key.js"></script> <script type="module" src="./script.js"></script> <script type="module" src="./config.js"></script>サンプルアプリ上にChartを生成する
兎にも角にもChartを作ります。
詳しくはChart.js公式ドキュメントをご覧になってください...なんですが、Chartを生成するためには
- canvas要素を作成する
- canvas要素とChart用の設定オブジェクトを引数に渡してnew Chart()する
必要があります。この処理をサンプルコードに追加します。
index.html<div class="chart" id="js-chart"></div>script.jsconst Chart = window.Chart; // ... const ctx = document.createElement('canvas'); ctx.id = 'canvas_' + key; chartArea.appendChild(ctx); const chart = new Chart(ctx, value);Chart用configはまだ作っていないので、まだこのコードは動きません。
config.js
というChart用設定ファイルを作っています。Chart用設定ファイルを作成する
Chart.jsとchartjs-plugin-streamingの公式ドキュメントに従って書いていきます。
config.js// 簡略化のため一部のみ抜粋 const defaultConfig = { type: 'line', data: { datasets: [], }, options: { title: { display: true, }, scales: { xAxes: [ { type: 'realtime', time: { unit: 'minute', }, }, ], yAxes: [ { ticks: { beginAtZero: true, sampleSize: 5, }, }, ], }, plugins: { streaming: { duration: 180000, // 180000ミリ秒(5分)のデータを表示 refresh: 5000, frameRate: 5, }, }, }, }; const RECEIVED_TRAFFIC = deepCopy({}, defaultConfig); RECEIVED_TRAFFIC.data.datasets = [ { label: 'audio', data: [], backgroundColor: 'rgba(0, 0, 0, 0)', borderColor: 'rgba(255, 99, 132, 0.5)', pointRadius: 1, }, { label: 'video', data: [], backgroundColor: 'rgba(0, 0, 0, 0)', borderColor: 'rgba(54, 162, 235, 0.5)', pointRadius: 1, }, ]; RECEIVED_TRAFFIC.options.title.text = '受信ビットレート(Kbps)'; export const config = { RECEIVED_TRAFFIC, // ... } function deepCopy(src, dst) { return Object.assign(src, JSON.parse(JSON.stringify(dst)));大事なのは
defaultConfig
の設定部分。
webrtc-internalライクなチャートを表示したいのであれば、横軸は時間軸、縦軸は統計値のリアルタイム折れ線チャートを作る必要があるため、その設定を書きます。
あとは取りたい各統計情報にdefaultConfig
をディープコピーして、それぞれデータラベルや折れ線の色を調整したりタイトルを設定したりするなどの細かい作業です。描画処理を書く
先程の設定ファイルをimportして、描画処理を書きます。
updateChart
関数がそれに当たります。script.js// 簡略化のため一部のみ抜粋 import { config } from './config.js'; const { RTCStatsMoment } = window.RTCStatsWrapper; const Chart = window.Chart; const Peer = window.Peer; const charts = new Map(); let timerId; (async function main() { const chartArea = document.getElementById('js-chart'); // ... // New each charts for WebRTC stats for (const [key, value] of Object.entries(config)) { const ctx = document.createElement('canvas'); ctx.id = 'canvas_' + key; chartArea.appendChild(ctx); const chart = new Chart(ctx, value); charts.set(key, chart); } // Register caller handler callTrigger.addEventListener('click', () => { // ... mediaConnection.on('stream', async stream => { // ... // Update chart for stats timerId = _updateCharts(mediaConnection); }); mediaConnection.once('close', () => { // ... // Stop drawing for stats clearInterval(timerId); }); closeTrigger.addEventListener('click', () => { // ... clearInterval(timerId); }); }); peer.once('open', id => (localId.textContent = id)); // Register callee handler peer.on('call', mediaConnection => { // ... mediaConnection.on('stream', async stream => { // ... timerId = _updateCharts(mediaConnection); }); mediaConnection.once('close', () => { // ... clearInterval(timerId); }); closeTrigger.addEventListener('click', () => { // ... clearInterval(timerId); }); }); // ... async function _updateCharts(mc) { const moment = new RTCStatsMoment(); const peerConnection = await mc.getPeerConnection(); return setInterval(async () => { const stats = await peerConnection.getStats(); moment.update(stats); const report = moment.report(); for (const [key, chart] of charts) { switch (key) { case 'RECEIVED_TRAFFIC': chart.data.datasets[0].data.push({ t: new Date(), y: report.receive.audio.bitrate / 1024, // bps -> Kbps }); chart.data.datasets[1].data.push({ t: new Date(), y: report.receive.video.bitrate / 1024, }); break; case 'SENT_TRAFFIC': // ... default: console.warn('No value!'); break; } chart.update(); } }, 5000); } })();引数にMediaConnectionを持っていたり、関数名と直接関係のない処理を書いていたり、計算処理がハードコードだったりと、かなりイケてないですがやりたかったことを伝えると...。
仕組みとしては、
1. 取得したchartをMap Objectに格納し
2. 5秒ごとにgetStats()
し
3. 標準化したStatsの瞬間値を取得し
4. for-switch文でchartの種類を判別して
5. それぞれ適したデータ{t, y}
をchartのdataにPushしています。この処理により、各statsごとにリアルタイムチャートを表示させることが可能です。
まとめというか感想
- 雑なQiitaになってしまいすみませんでした!
- 各ブラウザアプリ内にstatsグラフを表示させることは可能です
- chartjs-plugin-streamingがここ数ヶ月メンテされていない雰囲気なので運用レベルで可視化を考えるなら他のツールのほうが良いかも
- 投稿日:2019-12-13T16:05:37+09:00
rtcstats-wrapperとChart.jsでWebRTC統計情報を可視化してみた
これはなに?
前回の記事
https://qiita.com/monmee/items/77310482b0a6a034a887
で紹介したrtcstats-wrapperですが、具体的にどう使えるん?みたいなところは紹介してなかったので、一例を簡単に作ってみた記事。
rtcstats-wrapperの想定される使い方として
- 例えば、アプリ内にwebrtc-internal的な統計グラフを表示、ユーザに通信状況をFBする
- 例えば、パケットロス率が異常値に達したらSlackに通知するなどのWebRTC通信の監視体制を作る
- 例えば、取得した統計情報とユーザIDを紐付けて、各ユーザの通信環境パフォーマンスの解析材料とする
などがあります。今回は一番上の
例えば、アプリ内にwebrtc-internal的な統計グラフを表示、ユーザに通信状況をFBする
をやってみました。
できたもの
Gistにソースコードをupしています。
とりあえず動かしたれ!精神でやってる気合のコードでお見苦しいところもあるかと思いますが参考程度に見てください。index.html
sciprt.js
config.js左がSafari、右がFirefoxです。定期的にstatsを取得してグラフをリアタイで動かしてます。
画像では受信/送信トラフィックのみ表示してますが、rtcstats-wrapperから取得できる値であれば他の統計情報も取れます。(本当に取れるかは各ブラウザのgetStats()
実装状況によるけど)ミニ
chrome://webrtc-internals
が他ブラウザでも見れるという感じです。必要なもの
rtcstats-wrapper
各ブラウザの
getStats()
を標準化して統計情報の瞬間値を計測してくれるラッパー。WebRTCアプリ
rtcstats-wrapperは現在ブラウザP2Pのみ対応です。
SkyWayのサンプルアプリを使いました。データ可視化ライブラリ
今回使った外部ライブラリはこれ。
- Chart.js
- chartjs-plugin-streaming
- moment.js時系列データをリアルタイムにブラウザに出力できれば何でも良いと思います。
例えば、グラフ描画系ならChart.js以外にも、d3.js、Google Chartらへんが候補としてありそうです。
今回はリアルタイムデータ向けChart.jsプラグインのchartjs-plugin-streamingをChart.jsと併用しています。時間軸がスムーズに動かなかったりDeprecated Warningがコンソール表示されるあたり、chartjs-plugin-streamingはChart.js v2.8.0以降に対応してなさそうなので、今回はChart.js v2.7.3を使用しました。
Chart.jsで時系列グラフを作るためにmoment.jsも必要らしいのでそちらも必要。
前準備
使用するライブラリをダウンロードします。
Vanilla JSでimport/export文を使えるように最後の2ファイルにはtype="module"
を追加しています。
config.jsについては後述します。index.html<script src="/node_modules/rtcstats-wrapper/dist/rtcstats-wrapper.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@2.7.3/dist/Chart.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@latest/dist/chartjs-plugin-streaming.min.js"></script> <script src="//cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script> <script src="../_shared/key.js"></script> <script type="module" src="./script.js"></script> <script type="module" src="./config.js"></script>サンプルアプリ上にChartを生成する
兎にも角にもChartを作ります。
詳しくはChart.js公式ドキュメントをご覧になってください...なんですが、Chartを生成するためには
- canvas要素を作成する
- canvas要素とChart用の設定オブジェクトを引数に渡してnew Chart()する
必要があります。この処理をサンプルコードに追加します。
index.html<div class="chart" id="js-chart"></div>script.jsconst Chart = window.Chart; // ... const ctx = document.createElement('canvas'); ctx.id = 'canvas_' + key; chartArea.appendChild(ctx); const chart = new Chart(ctx, value);Chart用configはまだ作っていないので、まだこのコードは動きません。
config.js
というChart用設定ファイルを作っています。Chart用設定ファイルを作成する
Chart.jsとchartjs-plugin-streamingの公式ドキュメントに従って書いていきます。
config.js// 簡略化のため一部のみ抜粋 const defaultConfig = { type: 'line', data: { datasets: [], }, options: { title: { display: true, }, scales: { xAxes: [ { type: 'realtime', time: { unit: 'minute', }, }, ], yAxes: [ { ticks: { beginAtZero: true, sampleSize: 5, }, }, ], }, plugins: { streaming: { duration: 180000, // 180000ミリ秒(5分)のデータを表示 refresh: 5000, frameRate: 5, }, }, }, }; const RECEIVED_TRAFFIC = deepCopy({}, defaultConfig); RECEIVED_TRAFFIC.data.datasets = [ { label: 'audio', data: [], backgroundColor: 'rgba(0, 0, 0, 0)', borderColor: 'rgba(255, 99, 132, 0.5)', pointRadius: 1, }, { label: 'video', data: [], backgroundColor: 'rgba(0, 0, 0, 0)', borderColor: 'rgba(54, 162, 235, 0.5)', pointRadius: 1, }, ]; RECEIVED_TRAFFIC.options.title.text = '受信ビットレート(Kbps)'; export const config = { RECEIVED_TRAFFIC, // ... } function deepCopy(src, dst) { return Object.assign(src, JSON.parse(JSON.stringify(dst)));大事なのは
defaultConfig
の設定部分。
webrtc-internalライクなチャートを表示したいのであれば、横軸は時間軸、縦軸は統計値のリアルタイム折れ線チャートを作る必要があるため、その設定を書きます。
あとは取りたい各統計情報にdefaultConfig
をディープコピーして、それぞれデータラベルや折れ線の色を調整したりタイトルを設定したりするなどの細かい作業です。描画処理を書く
先程の設定ファイルをimportして、描画処理を書きます。
updateChart
関数がそれに当たります。script.js// 簡略化のため一部のみ抜粋 import { config } from './config.js'; const { RTCStatsMoment } = window.RTCStatsWrapper; const Chart = window.Chart; const Peer = window.Peer; const charts = new Map(); let timerId; (async function main() { const chartArea = document.getElementById('js-chart'); // ... // New each charts for WebRTC stats for (const [key, value] of Object.entries(config)) { const ctx = document.createElement('canvas'); ctx.id = 'canvas_' + key; chartArea.appendChild(ctx); const chart = new Chart(ctx, value); charts.set(key, chart); } // Register caller handler callTrigger.addEventListener('click', () => { // ... mediaConnection.on('stream', async stream => { // ... // Update chart for stats timerId = _updateCharts(mediaConnection); }); mediaConnection.once('close', () => { // ... // Stop drawing for stats clearInterval(timerId); }); closeTrigger.addEventListener('click', () => { // ... clearInterval(timerId); }); }); peer.once('open', id => (localId.textContent = id)); // Register callee handler peer.on('call', mediaConnection => { // ... mediaConnection.on('stream', async stream => { // ... timerId = _updateCharts(mediaConnection); }); mediaConnection.once('close', () => { // ... clearInterval(timerId); }); closeTrigger.addEventListener('click', () => { // ... clearInterval(timerId); }); }); // ... async function _updateCharts(mc) { const moment = new RTCStatsMoment(); const peerConnection = await mc.getPeerConnection(); return setInterval(async () => { const stats = await peerConnection.getStats(); moment.update(stats); const report = moment.report(); for (const [key, chart] of charts) { switch (key) { case 'RECEIVED_TRAFFIC': chart.data.datasets[0].data.push({ t: new Date(), y: report.receive.audio.bitrate / 1024, // bps -> Kbps }); chart.data.datasets[1].data.push({ t: new Date(), y: report.receive.video.bitrate / 1024, }); break; case 'SENT_TRAFFIC': // ... default: console.warn('No value!'); break; } chart.update(); } }, 5000); } })();引数にMediaConnectionを持っていたり、関数名と直接関係のない処理を書いていたり、計算処理がハードコードだったりと、かなりイケてないですがやりたかったことを伝えると...。
仕組みとしては、
1. 取得したchartをMap Objectに格納し
2. 5秒ごとにgetStats()
し
3. 標準化したStatsの瞬間値を取得し
4. for-switch文でchartの種類を判別して
5. それぞれ適したデータ{t, y}
をchartのdataにPushしています。この処理により、各statsごとにリアルタイムチャートを表示させることが可能です。
まとめというか感想
- 雑なQiitaになってしまいすみませんでした!
- 各ブラウザアプリ内にstatsグラフを表示させることは可能です
- chartjs-plugin-streamingがここ数ヶ月メンテされていない雰囲気なので運用レベルで可視化を考えるなら他のツールのほうが良いかも
- 投稿日:2019-12-13T15:57:17+09:00
そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。
PlayCanvasアドベントカレンダー11日目!
投稿日が遅れてしまいましたが、この記事はPlayCanvasアドベントカレンダー 12月11日の記事となります。
PhotonのJS SDKとゲームエンジンを組み合わせて色々できそうだなと思っていたのでアドベントカレンダーを機にもう一度触ってみます。
HTML5 VTuber LIVEシステムとか作りたいJavaScriptでリアルタイム通信するゲームを作る
JavaScriptはよく触ってるけど、そういえばリアルタイムの通信/対戦ゲームってどうやって作るのってなった時に参考になるかもしれないです。
PlayCanvasでマルチ対戦のゲームを作るために今回はPhotonというネットワークエンジンを使用して作成します。役割としては描画の部分はPlayCanvas
、リアルタイム通信の部分についてはPhoton
を使用して作っていきます。今回デモとして作成したこちらのゲームは[WASD]で移動ができるシンプルなゲームとなっております。
PhotonとPlayCanvas
— はが (@Mxcn3) December 13, 2019
[W A S D]で移動(スマホはタッチで飛ぶだけ)#playcanvas #2窓https://t.co/mPDM21LfmePlayCanvasについて
PlayCanvasはJavaScriptで記述されたWebGLのエンジンで、最近GitHubのスター数が5000を超えました。
OSSのEngineと、SaaS型のエディターを兼ね備えているゲームエンジンです。engineについては、Three.js, Babylon.jsなどに似た使い方を使用することができます。エディターはビジュアルエディターとコードエディターがあり、エディターはクラウド上でプロジェクトの作成から公開までが出来るもので、FBX
やOBJ
といった3Dモデルの素材については、ブラウザへドラッグアンドドロップをすることでGUIから操作・配置ができるエディターとなっております。Photonについて
Photonについては、Unity多く使われているようですが、JavaScriptのSDKもありますので、Photon JavaScript SDKを使用してマルチ対戦のゲームを作っていきます。
Photonは20CCU(同時接続)までであれば、無料で使用できます。
https://www.photonengine.com/ja/photon
PlayCanvasとPhotonを組み合わせる
PlayCanvasとPhotonを組み合わせるにはこちらのリポジトリを参考になります。
https://github.com/utautattaro/Photon-for-PlayCanvas
Photon x PlayCanvasでできること
3Dや2Dのリアルタイムゲームを作ることができます。こちらのタンクのゲームは4人がリアルタイム1つのルームに入り対戦できるゲームです。
プロジェクトをフォークして作成する
上記のリポジトリを参考に、自分でプロジェクトを使用して作ることもできますが、PlayCanvasには便利なForkという機能があるのでそちらを利用して作ってみます。
1. Forkする
こちらのデモ用のPlayCanvasのプロジェクトはPublicなプロジェクトとなっておりますので、PlayCanvasにログインしアクセスしていただくと、Forkできます。
2. PlayCanvas EditorのAppIdを設定する
今回簡単なプロジェクトを作る上では、AppIdを設定することができればマルチプレイ対戦ができるようになっています。AppIdの設定にはPhotonのIDを作る必要があるので、設定します。
a. Photonに登録する
- Photon 公式サイトにアクセスして登録をします。
b. PhotonからAppId取得する
- Photonのダッシュボードから新規アプリを作成し、AppIdを取得します。
c. AppIdをPlayCanvas上に設定する
- Forkしたプロジェクトに入る
- コピーしたAppIdを貼り付ける
PhotonのJavaScriptSDKをダウンロード
PlayCanvasでPhotonを使用するためにJavaScriptのSDKをダウンロードします。
- PhotonのSDKの最新版をダウンロード
https://www.photonengine.com/ja/sdks
① SDKを解凍し\photon-javascript-sdk_v4-1-0-1\libの中にある
Photon-Javascript_SDK
をPlayCanvasにアップロード② SETTINGSをクリック
③ Photon-Javascript_SDKのSCRIPT LOADIGN ORDERを一番上に!
しかしこのままだとPlayCanvasで使用する際にPhotonのSDKを読み込む前にPhotonのAPIを使用するプログラムが書いてあるため、SETTIGNSからSCRIPTのLOADING ORDERを変更します。
ゲームを起動する
PlayCanvasではエディター上には開発時に使用する
Launch
とリリースする際に使用するPublish
の2つの機能があります。a. Launchで確認する
開発時にはこちらのLaunchボタンを押して確認します。
スクリプトについて
init.js
起動Photonの起動を行っています。
index.html
,style.css
を読み込みUIのセットアップも行います。app.js
app.jsにてPhotonとPlayCanvasの同期をしています。Photonには様々なライフサイクルで発行されるイベントがありますが、今回使用したものについて説明します。
/*jshint esversion: 6, asi: true, laxbreak: true*/ const App = pc.createScript('app'); App.prototype.initialize = function() { this.photon = this.app.root.children[0].photon; this.photon.setLogLevel(999); this.photon.onJoinRoom = () => { Object.values(this.photon.actors).map(event => { const { isLocal, actorNr } = event; if (isLocal) { } else { const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); entity.setPosition(0, 1, 0); entity.tags.add(`${actorNr}`); this.app.root.addChild(entity); } }); }; this.photon.onActorJoin = event => { const { isLocal, actorNr } = event; if (isLocal) { } else { const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); entity.setPosition(0, 1, 0); entity.tags.add(`${actorNr}`); this.app.root.addChild(entity); } }; this.photon.onEvent = (code, content, actorNr) => { const entities = this.app.root.findByTag(`${actorNr}`); const [entity] = entities; switch (code) { case 1: { const { x, y, z } = content; entity.setLocalPosition(x, y, z); break; } case 2: { const { x, y, z, w } = content; entity.setLocalRotation(x, y, z, w); break; } default: { break; } } }; };photon.onJoinRoom
onJoinRoomはルームに入った際の処理になります。今回のような作りでは呼ばれるタイミング
- ルームを新しく作ったとき
- 既存のルームに入る時
- ルームに入った時
- 他のプレイヤーを取得
- 新しいエンティティ("Model:Box")としてゲーム画面に配置
// 1.ルームに入った時 this.photon.onJoinRoom = () => { //2. 他のプレイヤーを取得 Object.values(this.photon.actors).map(event => { const { isLocal, actorNr } = event; if (isLocal) { } else { // 新しいエンティティを作成 const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); // 新しく生成したエンティティのポジションの設定とタグを付与している entity.setPosition(0, 1, 0); // x, y, z entity.tags.add(`${actorNr}`); // PlayCanvasの画面上に配置する //3. 新しいエンティティ("Model:Box")としてゲーム画面に配置 this.app.root.addChild(entity); } }); };photon.onActorJoin
onActorJoinは自分の入っているルームに他のプレイヤーが参加してきたときの処理になります。
- 他のプレイヤーが入ってきた時
- 新しいエンティティを作成
- 新しいエンティティ("Model:Box")としてゲーム画面に配置
// 1.他のプレイヤーが入ってきた時 this.photon.onActorJoin = event => { // isLocal: 自分であるかの判定 // actorNr: 入った順に1から振られる、ユニークな番号 const { isLocal, actorNr } = event; // 自分自身であるかの判定 if (isLocal) { } else { // 2. 新しいエンティティを作成 const entity = new pc.Entity(); entity.addComponent("model", { type: "box" }); // ポジションとタグを設定 entity.setPosition(0, 1, 0); entity.tags.add(`${actorNr}`); // 3. 新しいエンティティ("Model:Box")としてゲーム画面に配置 this.app.root.addChild(entity); } };photon.onEvent
onEventは、今回は
player.js
により、データが送信された場合に呼び出されます。
actorNr
のタグを元にエンティティ
を検索Code
によってPosition
かRotation
かの判定を行うthis.photon.onEvent = (code, content, actorNr) => { // 1. `actorNr`のタグを元に`エンティティ`を検索 const entities = this.app.root.findByTag(`${actorNr}`); const [entity] = entities; //2. `Code`によって`Position`か`Rotation`かの判定を行う switch (code) { case 1: { const { x, y, z } = content; entity.setLocalPosition(x, y, z); break; } case 2: { const { x, y, z, w } = content; entity.setLocalRotation(x, y, z, w); break; } default: { break; } } };player.js
Player.jsはキーボードの操作が押された際にPhotonサーバーにデータを送るものになります。
/*jshint esversion: 6, asi: true, laxbreak: true*/ const Player = pc.createScript("player"); // PlayCanvas Editor上で使用するためにAttributesを作成 // Attributesの説明 // https://developer.playcanvas.com/ja/user-manual/scripting/script-attributes/ Player.attributes.add("moveSpeed", { type: "number", default: 0.1 }); Player.attributes.add("rotateSpeed", { type: "number", default: 2 }); const move = (direction, entity, self) => { const { photon } = self; switch (direction) { case "up": { entity.translateLocal(0, 0, -self.moveSpeed); break; } case "down": { entity.translateLocal(0, 0, self.moveSpeed); break; } default: { break; } } // send position // コード番号 1で現在のEntityのポジションを送信 photon.raiseEvent(1, entity.getLocalPosition()); }; const rotate = (direction, entity, self) => { const { photon } = self; switch (direction) { case "left": { entity.rotate(0, -self.rotateSpeed, 0); break; } case "right": { entity.rotate(0, self.rotateSpeed, 0); break; } default: { break; } } // send rotation // コード番号 2で現在のEntityのポジションを送信 photon.raiseEvent(2, entity.getLocalRotation()); }; Player.prototype.initialize = function () { this.photon = this.app.root.children[0].photon; if (this.app.touch) { this.app.touch.on( pc.EVENT_TOUCHSTART, () => { const { photon } = this const { x, y, z } = this.entity.getPosition(); this.entity.rigidbody.teleport(x, y + 0.5, z); photon.raiseEvent(1, this.entity.getLocalPosition()); photon.raiseEvent(2, this.entity.getLocalRotation()); }, null ); } }; // update code called every frame Player.prototype.update = function (dt) { const { keyboard } = this.app; // 移動のキーが押されていたら if (keyboard.isPressed(pc.KEY_W) || keyboard.isPressed(pc.KEY_UP)) { move("up", this.entity, this); } else if (keyboard.isPressed(pc.KEY_S) || keyboard.isPressed(pc.KEY_DOWN)) { move("down", this.entity, this); } // 回転のキーが押されたら if (keyboard.isPressed(pc.KEY_A) || keyboard.isPressed(pc.KEY_LEFT)) { rotate("right", this.entity, this); } else if (keyboard.isPressed(pc.KEY_D) || keyboard.isPressed(pc.KEY_RIGHT)) { rotate("left", this.entity, this); } };photon.raiseEvent
raiseEventを使用して任意のタイミングでデータの送信を行います。今回は移動が行われた時と回転が行われた際に現在の場所を送信しています。
raiseEvent(eventCode, data, options)型はそれぞれ
eventCode:number object: object options: object移動時
.... // send position // コード番号 1で現在のEntityのポジションを送信 photon.raiseEvent(1, entity.getLocalPosition()); };回転時
... // send rotation // コード番号 2で現在のEntityのポジションを送信 photon.raiseEvent(2, entity.getLocalRotation()); };参考
initialize
PlayCanvasのスクリプトのライフサイクルには、スクリプト一度だけ呼び出される
initialize
、毎フレーム呼び出されるUpdate
があります。
- Photonのプロパティを代入
- タッチされた際に発火するイベントの定義
Player.prototype.initialize = function () { // Rootエンティティからphotonプロパティを取得 // 1. Photonのプロパティを代入 this.photon = this.app.root.children[0].photon; // タッチ操作が可能だったら if (this.app.touch) { // タッチ操作が行われたら // PlayCanvas: タッチについて // https://support.playcanvas.jp/hc/ja/articles/227190908 // 2. タッチされた際に発火するイベントの定義 // PlayCanvas: イベント種類について // https://developer.playcanvas.com/ja/user-manual/scripting/communication/ this.app.touch.on( pc.EVENT_TOUCHSTART, () => { const { photon } = this const { x, y, z } = this.entity.getPosition(); this.entity.rigidbody.teleport(x, y + 0.5, z); // コード1で現在のポジションを送信 photon.raiseEvent(1, this.entity.getLocalPosition()); // コード2で現在の回転を送信 photon.raiseEvent(2, this.entity.getLocalRotation()); }, null ); } };update
update
はフレームごとに呼ばれます
- 移動のキーが押されていたら
- 回転のキーが押されたら
Player.prototype.update = function (dt) { const { keyboard } = this.app; // 1. 移動のキーが押されていたら if (keyboard.isPressed(pc.KEY_W) || keyboard.isPressed(pc.KEY_UP)) { // 移動 move("up", this.entity, this); } else if (keyboard.isPressed(pc.KEY_S) || keyboard.isPressed(pc.KEY_DOWN)) { // 移動 move("down", this.entity, this); } // 2. 回転のキーが押されたら if (keyboard.isPressed(pc.KEY_A) || keyboard.isPressed(pc.KEY_LEFT)) { // 回転 rotate("right", this.entity, this); } else if (keyboard.isPressed(pc.KEY_D) || keyboard.isPressed(pc.KEY_RIGHT)) { // 回転 rotate("left", this.entity, this); } };起動
ゲーム起動すると
index.html
とstyle.css
に定義されているUIと、ゲームが表示されます。b. PublishをしてURLを共有する
Launch
はログインしているユーザーしか閲覧できないのでこちらから公開用のURLを発行することで、インターネット上で共有できます。おわりに
リアルタイムの通信をするゲームもかなり簡単に作れるそうですね。
もう少し興味がある方は、PhotonSDKのAPIはplayer.js
で記述されているのでそちらをいじると色々できます。→ player.jsをいじった記事を書きました
そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。その2https://qiita.com/yushimatenjin/items/eb4311c3640a20bbdfb9
以下PlayCanvas開発で参考になりそうな記事の一覧です。
入門
- PlayCanvas入門- モデルの作成~ゲームに入れ込むまで
- JavaScriptでスロットを実装する。【PlayCanvas】
- 3Dモデルのビューワーを3分で作る【初めてのPlayCanvas】
- PlayCanvasのコードエディターでes6に対応する
- Gulpのプラグインを書いたらPlayCanvasでの開発がめちゃくちゃ便利になった
- PlayCanvas Editorに外部スクリプトを読み込む新機能が追加されたので開発方法を考える。- Reduxを組み込むその他の記事はこちらになります。
- AR年賀状を作る
- React Native + PlayCanvasを使ってスマートフォンゲームを爆速で生み出す
- PlayCanvasのエディター上でHTML, CSSを組み込む方法
- 【iOS13】新しくなったWebVRの使い方PlayCanvasのユーザー会のSlackを作りました!
少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください!
- 投稿日:2019-12-13T15:57:17+09:00
そういえば、JavaScriptでリアルタイム通信のゲームってどうやって作るのってなったとき。[PlayCanvas x Photon]
PlayCanvasアドベントカレンダー11日目!
投稿日が遅れてしまいましたが、この記事はPlayCanvasアドベントカレンダー 12月11日の記事となります。
PhotonのJS SDKとゲームエンジンを組み合わせて色々できそうだなと思っていたのでアドベントカレンダーを機にもう一度触ってみます。
HTML5 VTuber LIVEシステムとか作りたいJavaScriptでリアルタイム通信するゲームを作る
JavaScriptはよく触ってるけど、そういえばリアルタイムの通信/対戦ゲームってどうやって作るのってなった時に参考になるかもしれないです。
PlayCanvasでマルチ対戦のゲームを作るために今回はPhotonというネットワークエンジンを使用して作成します。役割としては描画の部分はPlayCanvas
、リアルタイム通信の部分についてはPhoton
を使用して作っていきます。今回デモとして作成したこちらのゲームは[WASD]で移動ができるシンプルなゲームとなっております。
PhotonとPlayCanvas
— はが (@Mxcn3) December 13, 2019
[W A S D]で移動(スマホはタッチで飛ぶだけ)#playcanvas #2窓https://t.co/mPDM21LfmePlayCanvasについて
PlayCanvasはJavaScriptで記述されたWebGLのエンジンで、最近GitHubのスター数が5000を超えました。
OSSのEngineと、SaaS型のエディターを兼ね備えているゲームエンジンです。engineについては、Three.js, Babylon.jsなどに似た使い方を使用することができます。エディターはビジュアルエディターとコードエディターがあり、エディターはクラウド上でプロジェクトの作成から公開までが出来るもので、FBX
やOBJ
といった3Dモデルの素材については、ブラウザへドラッグアンドドロップをすることでGUIから操作・配置ができるエディターとなっております。Photonについて
Photonについては、Unity多く使われているようですが、JavaScriptのSDKもありますので、Photon JavaScript SDKを使用してマルチ対戦のゲームを作っていきます。
Photonは20CCU(同時接続)までであれば、無料で使用できます。
https://www.photonengine.com/ja/photon
PlayCanvasとPhotonを組み合わせる
PlayCanvasとPhotonを組み合わせるにはこちらのリポジトリを参考になります。
https://github.com/utautattaro/Photon-for-PlayCanvas
Photon x PlayCanvasでできること
3Dや2Dのリアルタイムゲームを作ることができます。こちらのタンクのゲームは4人がリアルタイム1つのルームに入り対戦できるゲームです。
プロジェクトをフォークして作成する
上記のリポジトリを参考に、自分でプロジェクトを使用して作ることもできますが、PlayCanvasには便利なForkという機能があるのでそちらを利用して作ってみます。
1. Forkする
こちらのデモ用のPlayCanvasのプロジェクトはPublicなプロジェクトとなっておりますので、PlayCanvasにログインしアクセスしていただくと、Forkできます。
2. PlayCanvas EditorのAppIdを設定する
今回簡単なプロジェクトを作る上では、AppIdを設定することができればマルチプレイ対戦ができるようになっています。AppIdの設定にはPhotonのIDを作る必要があるので、設定します。
a. Photonに登録する
- Photon 公式サイトにアクセスして登録をします。
b. PhotonからAppId取得する
- Photonのダッシュボードから新規アプリを作成し、AppIdを取得します。
c. AppIdをPlayCanvas上に設定する
- Forkしたプロジェクトに入る
- コピーしたAppIdを貼り付ける
PhotonのJavaScriptSDKをダウンロード
PlayCanvasでPhotonを使用するためにJavaScriptのSDKをダウンロードします。
- PhotonのSDKの最新版をダウンロード
https://www.photonengine.com/ja/sdks
① SDKを解凍し\photon-javascript-sdk_v4-1-0-1\libの中にある
Photon-Javascript_SDK
をPlayCanvasにアップロード② SETTINGSをクリック
③ Photon-Javascript_SDKのSCRIPT LOADIGN ORDERを一番上に!
しかしこのままだとPlayCanvasで使用する際にPhotonのSDKを読み込む前にPhotonのAPIを使用するプログラムが書いてあるため、SETTIGNSからSCRIPTのLOADING ORDERを変更します。
ゲームを起動する
PlayCanvasではエディター上には開発時に使用する
Launch
とリリースする際に使用するPublish
の2つの機能があります。a. Launchで確認する
開発時にはこちらのLaunchボタンを押して確認します。
起動
ゲーム起動すると
index.html
とstyle.css
に定義されているUIと、ゲームが表示されます。b. PublishをしてURLを共有する
Launch
はログインしているユーザーしか閲覧できないのでこちらから公開用のURLを発行することで、インターネット上で共有できます。おわりに
リアルタイムの通信をするゲームもかなり簡単に作れるそうですね。
もう少し興味がある方は、PhotonSDKのAPIはplayer.js
で記述されているのでそちらをいじると色々できます。以下PlayCanvas開発で参考になりそうな記事の一覧です。
入門
- PlayCanvas入門- モデルの作成~ゲームに入れ込むまで
- JavaScriptでスロットを実装する。【PlayCanvas】
- 3Dモデルのビューワーを3分で作る【初めてのPlayCanvas】
- PlayCanvasのコードエディターでes6に対応する
- Gulpのプラグインを書いたらPlayCanvasでの開発がめちゃくちゃ便利になった
- PlayCanvas Editorに外部スクリプトを読み込む新機能が追加されたので開発方法を考える。- Reduxを組み込むその他の記事はこちらになります。
- AR年賀状を作る
- React Native + PlayCanvasを使ってスマートフォンゲームを爆速で生み出す
- PlayCanvasのエディター上でHTML, CSSを組み込む方法
- 【iOS13】新しくなったWebVRの使い方PlayCanvasのユーザー会のSlackを作りました!
少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください!
- 投稿日:2019-12-13T15:54:49+09:00
スプラトゥーン2のガチマ情報を常に見たかった
表題を悩んだ結果、PWAでホーム画面にアイコン追加でいっか。
※mithrilを使う必要性がなくなったのでmithrilは別途…
できたもの
https://pwa.manic-design-lab.dev/
やりたかったこと
・push7というASPを利用させていただき、push通知が飛ばせるように
・ホーム画面へ追加
・手動でmanifest.json、service-workerを書かないでwebpackのpluginだけで完結させるできなかったこと
・キャッシュコントールが雑多で終わった
・ゆえのオフライン時のアップデート挙動の確立
・細かい設計
ガチマ潜りすぎてでき(ry力添えいただいた
・splatoon2ステージ情報APIを作ってくださっている方
リポジトリはこちら
https://github.com/Gits-migii/pwa_spla
以下ServiceWorker + manifest.json周りの説明
new GenerateSW({ swDest: 'sw.js', clientsClaim: true, skipWaiting: true, importScripts:['push7-worker.js'], runtimeCaching: [{ urlPattern: '/images/stage/', handler: 'NetworkFirst', options: { networkTimeoutSeconds: 10, cacheName: 'my-api-cache', expiration: { maxEntries: 64, maxAgeSeconds: 60, }, backgroundSync: { name: 'my-queue-name', options: { maxRetentionTime: 60 * 60, }, }, cacheableResponse: { statuses: [0, 200], headers: {'x-test': 'true'}, }, broadcastUpdate: { channelName: 'my-update-channel', }, fetchOptions: { mode: 'no-cors', }, matchOptions: { ignoreSearch: true, }, }, }, { urlPattern:new RegExp('^https://app\.splatoon2\.nintendo\.net/'), handler: 'StaleWhileRevalidate', options: { cacheableResponse: { statuses: [0, 200] } } }] })ほぼworkbox公式のそのままです。
和訳ほしいので他力本願してるところです。詰みかけたところ
webpack-dev-serverが参照している仮想HTMLにmanifest.jsonの入れ方がわからなくて詰んだ。
HtmlWebpackPluginのinjectをtrueにして解決webpack-pwa-manifest
こいつを使って、fingerprintなどで開発時のキャッシュ問題をクリアにしようとしていたところ3つくらい追加でpluginを入れる必要があり、めんどくせえ!!!となってfingerprintsをfalseにしておしまい。
単純にmanifestで必要な部分を列挙しています。所感
workboxがかんたんに使えるようになったので、だいぶPWA化するのは楽になったのではないでしょうか。
iOS、Android間もpush通知くらいまでは落ちましたし。
アドベントカレンダーというものに初めて参加してみましたが、もっと前から準備するべきでした(戒め)
来年はもっと…やるぞおおおおおおおお乱文失礼しました。
- 投稿日:2019-12-13T15:43:52+09:00
今日の日付をJavaScriptで表示させる方法
今日の日付は、JavaScriptで以下の通りに書きます。
const today = new Date(); const todayHtml = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate(); document.write('<p class="date">' + todayHtml + '</p>');HTMLとJavaScript全体のコードは以下の通りです。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>TODAY</title> <!--Bootstrap CDN ここから--> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous"> <!-- Optional theme --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous"> <!--Bootstrap CDN ここまで--> <link rel="stylesheet" href="styles.css"> </head> <body> <header> <h1>TODAY</h1> </header> <div class="container"> <script> const today = new Date(); const todayHtml = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate(); document.write('<p class="date">' + todayHtml + '</p>'); </script> </body> </html>【プログラマー未経験者の方へ】
ドットインストールが難しくて萎えている方や、模写コーディングが出来なくて悩んでいる方はUdemyがめっちゃおすすめです。
僕はプログラマー未経験者ですが、以下の記事の講座で模写コーディング、さらには自分でコーディングできるまでになりました。
https://greenberet.net/mosya-coding-udemy/
僕はコーディングできるまでかなり苦しみ、何度も挫折しかけましたが、Udemyに助けられました。
参考にしていただければ幸いです。
- 投稿日:2019-12-13T15:39:37+09:00
document.writeの使い方
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>TODAY</title> <!--Bootstrap CDN ここから--> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous"> <!-- Optional theme --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous"> <!--Bootstrap CDN ここまで--> <link rel="stylesheet" href="styles.css"> </head> <body> <header> <h1>TODAY</h1> </header> <div class="container"> <script> document.write('<p class="date">20xx/1/1</p>'); </script> </div> </body> </html>(''); についての説明
documentは「オブジェクト」 writeは「メソッド」 ('')はパラメータと呼ぶ。
日本語風に言うと、「画面上(オブジェクト)に( )(パラメータ)を書く(メソッド)という意味になる。const today = new Date(); を使う理由
変数とは設計図を箱の中に入れる作業。
今回の場合、設計図=new Date(); 箱=const today
となる。【プログラマー未経験者の方へ】
ドットインストールが難しくて萎えている方や、模写コーディングが出来なくて悩んでいる方はUdemyがめっちゃおすすめです。
僕はプログラマー未経験者ですが、以下の記事の講座で模写コーディング、さらには自分でコーディングできるまでになりました。
https://greenberet.net/mosya-coding-udemy/
僕はコーディングできるまでかなり苦しみ、何度も挫折しかけましたが、Udemyに助けられました。
参考にしていただければ幸いです。
- 投稿日:2019-12-13T14:37:47+09:00
Angularで、階層構造を持つJSONから入力フォームを動的に生成する
Angularで階層構造を持つJSONから入力フォームを動的に生成するコードを、GitHubで公開しました。
GitHubはこちら
dynamic-form-builder-recursive画面サンプル
※インデントで階層構造を表しています
入力フォームの設定用JSON
fields: any[] = [ { "id": "text1", "text": "text1", "type": "text" }, { "id": "radio1", "text": "radio1", "type": "radio", "options": [ { "text": "option1", "value": "option1" }, { "text": "option2", "value": "option2" }, { "text": "option3", "value": "option3" } ] }, { "id": "checkbox1", "text": "checkbox1", "type": "checkbox", "options": [ { "text": "option1", "value": "option1" }, { "text": "option2", "value": "option2" }, { "text": "option3", "value": "option3" } ] }, { "id": "text2", "text": "text2", "type": "text", "item": [ { "id": "text2-1", "text": "text2-1", "type": "text", "item": [ { "id": "text2-1-1", "text": "text2-1-1", "type": "text", "item": [ { "id": "text2-1-1-1", "text": "text2-1-1-1", "type": "text", "item": [ { "id": "text2-1-1-1-1", "text": "text2-1-1-1-1", "type": "text" } ] } ] } ] } ] } ];参考させていただいたコード
Angular 6 dynamic from builder with Reactive Forms.
参考というかほぼコピペですがwww
ポイント
入力フォームの設定用JSONと同じ階層のFormGroupを作成
ngOnInit() { // FormControl生成 let fieldsCtrls:any = this.walkJSON(this.fields, function(item) { let fieldsCtrl: any = {}; if (item.type != 'checkbox') { fieldsCtrl = new FormControl(item.value || '', Validators.required) } else { let opts = {}; for (let opt of item.options) { opts[opt.value] = new FormControl(''); } fieldsCtrl = new FormGroup(opts) } return fieldsCtrl; }); this.form = new FormGroup(fieldsCtrls); } walkJSON(data, callback){ const formGroup: any = {}; data.forEach(item => { formGroup[item.id] = callback(item); if (item.item) { formGroup[item.id + '_child'] = new FormGroup(this.walkJSON(item.item, callback)); } }); return formGroup; }設定項目の中に子を持つ場合があるので、再帰的に処理します。
FormGroupには必ず名前を付けないといけないので、親のIDに「_child」を付けています。(ダサいのでなんとかしたいですが。。。)テンプレート側で、コンポーネントを再帰的に呼び出す
template: ` <ng-container [formGroup]="form"> <ng-container [ngSwitch]="field.type"> <div class="child"> <textbox *ngSwitchCase="'text'" [field]="field" [form]="form"></textbox> <checkbox *ngSwitchCase="'checkbox'" [field]="field" [form]="form"></checkbox> <radio *ngSwitchCase="'radio'" [field]="field" [form]="form"></radio> <div *ngIf="!isValid && isDirty">{{field.text}} is required</div> </div> </ng-container> <!-- 子を持つ場合 --> <ng-container *ngIf="field.item"> <div *ngFor="let childField of field.item" class="parent"> <field-builder [field]="childField" [form]="form.controls[field.id + '_child']"></field-builder> </div> </ng-container> </ng-container> `field-builderコンポーネントから、更にfield-builderコンポーネントを呼び出します。
その際に、同じ階層のFromGroupを指定します。感想
Angularは、設定ファイルから画面を生成するみたいなことにはあんまり向いてないんじゃないかと思ったけど、ちゃんと仕組みが用意されていた。
学習コストは高いが、慣れれば色々できそうな気がします。
- 投稿日:2019-12-13T13:54:23+09:00
jsでポケモン素早さ計算ツールを作ってみました
jsで素早さ計算ツールをつくってみた
ソード・シールドが発売して約1ヶ月がたちましたね。
ランクバトルの方も、1ヶ月という短い期間ですが、
なんとなく環境も固まりつつあるのかなと思います。その理由は登場するポケモンの少なさでしょう。
前作のサン・ムーンは約800体のポケモンが登場しましたが、
ソード・シールドでは半分の400体(内部データ含めず)。その中でもカプ・コケコの未登場や、メガゲンガーが使えなくなったことにより、
今まで素早さ調整の基準とされてきた130族のラインが、今作でさほど重要でなくなりました。
※今作で登場する130族はサンダース、コオリッポ(ナイスフェイス)のみまた、ダイマックス技であるダイジェットの登場により、飛行技を覚えるポケモンの素早さを1段階上げることができます。
実際、ギャラドスなんかも以前は竜舞型が多かったですが、今は飛び跳ねるをダイジェットにして素早さを上げる型が多いです。
何が言いたいかというと、ソード・シールドではより多くのポケモンが素早さを積みやすい環境にあり、
それを前提にした素早さ調整が大事になって来るのではないかと思われます。そういった時に、手早く素早さを実数値で確認できるツールがあると便利だなぁと思って、
今回JavaScriptで「すばやさ計算ツール」を作ってみることにしました。いざ、実装をしてみると...
う〜ん、むずかしい!!!会社の人にアドバイスをもらいながら、コツコツとつくりました。
そしてできたのがコチラ↓↓
すばやさ計算ツールソースの中身には自信がありませんが、UIに若干のこだわりアリ!
ファーストビューで実数値が確認できたり、努力値を±1づつボタンで微調整できます。まだ未完成ですが、とりあえず期限を決めていたのでアップする形になりました...このあと随時バージョンアップしていきます。
そのうちアプリ版も出したいと考えてますので、その時はまた紹介していこうと思います。
参考になるかわかりませんが、最後にコードのせときますね。jswindow.onload = function () { //最速出力ボタン let saisoku = document.getElementById('saisoku'); saisoku.addEventListener('click', function (e) { //デフォルトのクリック処理をブロックする e.preventDefault(); document.getElementById('doryokuchi').value = '252'; document.getElementById('seikaku').value = '1.1'; document.getElementById('kotaichi').value = '31'; document.getElementById("output_kotaichi").value = "31"; //計算の関数を呼び出し keisan() }); //準速出力ボタン let jyunsoku = document.getElementById("jyunsoku"); jyunsoku.addEventListener("click", function (e) { //デフォルトのクリック処理をブロックする e.preventDefault(); document.getElementById("doryokuchi").value = "252"; document.getElementById("seikaku").value = "1"; document.getElementById("kotaichi").value = "31"; document.getElementById("output_kotaichi").value = "31"; //計算の関数を呼び出し keisan() }); //無振出力ボタン let mufuri = document.getElementById("mufuri"); mufuri.addEventListener("click", function (e) { //デフォルトのクリック処理をブロックする e.preventDefault(); document.getElementById("doryokuchi").value = "0"; document.getElementById("seikaku").value = "1"; document.getElementById("kotaichi").value = "31"; document.getElementById("output_kotaichi").value = "31"; //計算の関数を呼び出し keisan() }); //下降出力ボタン let kakou = document.getElementById("kakou"); kakou.addEventListener("click", function (e) { //デフォルトのクリック処理をブロックする e.preventDefault(); document.getElementById("doryokuchi").value = "0"; document.getElementById("seikaku").value = "0.9"; document.getElementById("kotaichi").value = "31"; document.getElementById("output_kotaichi").value = "31"; //計算の関数を呼び出し keisan() }); //最遅出力ボタン let saichi = document.getElementById("saichi"); saichi.addEventListener("click", function (e) { //デフォルトのクリック処理をブロックする e.preventDefault(); document.getElementById("doryokuchi").value = "0"; document.getElementById("seikaku").value = "0.9"; document.getElementById("kotaichi").value = "0"; document.getElementById("output_kotaichi").value = "0"; //計算の関数を呼び出し keisan() }); // 素早さ種族値の取得 let syuzokuchi = document.getElementById("syuzokuchi"); // 素早さ種族値を変更したとき syuzokuchi.addEventListener("change", function (e) { e.preventDefault(); keisan() }); //個体値を変更した時に再計算 let kotaichi = document.getElementById("kotaichi"); kotaichi.addEventListener("change", function (e) { e.preventDefault(); keisan() }); //努力値の取得 let doryokuchi = document.getElementById("doryokuchi"); // 努力値を変更したとき doryokuchi.addEventListener("change", function (e) { e.preventDefault(); keisan() }); //性格補正値の取得 let seikaku = document.getElementById("seikaku"); // 性格補正値を変更したとき seikaku.addEventListener("change", function (e) { e.preventDefault(); keisan() }); //レベルの取得 let level = document.getElementById("level"); // レベルを変更したとき level.addEventListener("change", function (e) { e.preventDefault(); keisan() }); //道具を変更した時に再計算 let item = document.getElementsByName("item"); let itemValue = 1; item[0].addEventListener("change", function (e) { e.preventDefault(); if (item[0].checked) { itemValue = item[0].value } keisan() }); item[1].addEventListener("change", function (e) { e.preventDefault(); if (item[1].checked) { itemValue = item[1].value } keisan() }); item[2].addEventListener("change", function (e) { e.preventDefault(); if (item[2].checked) { itemValue = item[2].value } keisan() }); // 上昇値を変更した時に再計算 let jyousyouchi = document.getElementById("jyousyouchi"); // 上昇値を変更したとき jyousyouchi.addEventListener("change", function (e) { e.preventDefault(); keisan() }); //計算式 function keisan() { let keisan = ((parseInt(syuzokuchi.value, 10) * 2 + parseInt(kotaichi.value, 10) + parseInt(doryokuchi.value, 10) / 4) * (parseInt(level.value, 10) / 100) + 5) * parseFloat(seikaku.value, 10) * parseFloat(itemValue, 10) * parseFloat(jyousyouchi.value, 10); let jissuchi = document.getElementById("jissuchi"); jissuchi.value = parseInt(keisan); console.log(kotaichi.value) } }
- 投稿日:2019-12-13T13:36:15+09:00
アプリ管理者がJavaScriptカスタマイズを行うためのプラグイン
Qiitaに書くこと自体がひさしぶりになってしまいました。Spicaです!
機能拡張スタンダードAll-Inというプラグインを販売しております。最近ハマっているゲームはペルソナ5ロイヤルです。無印のペルソナ5もやりましたが、また最初から遊んでもやっぱり面白いっすね。あと十三機兵防衛圏というのを積んでいます。P5Rの1週目が終わったらとりかかる!
関係ない話はこのへんにして……kintoneの話を。
今回は基本に立ち返って、アプリのJavaScriptカスタマイズを手軽にする方法について書いていこうと思います。
ネタとしては軽めかなと思いましたが、kintoneカスタマイズのハードルを少しだけ下げてみようという試み、ということで。
プラグインも作りましたのでお使いいただければと。そもそもカスタマイズするためには
JavaScriptやCSSでkintone全体をカスタマイズする(kintoneヘルプ)
にある通り、JavascriptやCSSファイルの読み込みを設定できるのはシステム管理者のみとなっています。セキュリティ的には仕方ない部分もあるんですが、この縛りさえなければ……! と思ったことある方、多いんじゃないかなと思います。きっと多い。多いんじゃないかな。多いに決まってる。
ちゃんとアプリのアクセス権限を統制して編集者に気をつけるからアプリ管理者にもカスタマイズさせてくれよ、と。pluginの設定情報にスクリプトを保存したらいいのでは?
実装の仕組みですがしごく単純。
jQueryにglobalEvalというメソッドがあります。
渡された文字列をグローバルのスコープでスクリプトとして実行するというものです。
プラグインにスクリプトを保存して、こいつで実行してしまえというすんぽう。CSSも編集できるようにしました。
こっちはもっと単純に、<style>
タグの中身にしてbody
あたりに要素を追加してやるだけです。というわけでplugin作りました
なんとなくエディターふうに体裁を整えてますが、ここで書いたスクリプトはevalで実行されるので、デバッグは非常に大変です。
ここはひとつ、kintone開発者ライセンスを申請してですね、自分の環境でスクリプトを作り込んで完成したものをmasterデプロイする感覚で使うのがよきかなと思います。みんな開発者になればいいんですよ。以上です!
- 投稿日:2019-12-13T13:25:58+09:00
ReactFire v2 alphaを試してみる(Auth編)
はじめに
前回の記事では、react-firebase-hooksを試したのですが、12月6日のReact Day Berlinでは、ReactFireのプレゼンがありました。こちらはとても先進的で期待できます。
本記事では、Authの部分を移植してみたいと思います。
ReactFireとは
ReactFire v1は2016年5月のリリースを最後に、その後deprecateされ、Firebase JS SDKを直接使うようアナウンスされています。それが2019年7月よりv2として再度開発しているようです。
https://github.com/FirebaseExtended/reactfire
現在、masterブランチがv2になっています。
コーディング
import React, { Suspense, useState, useRef, useEffect } from "react"; import ReactDOM from "react-dom"; import "firebase/auth"; import { FirebaseAppProvider, useAuth, useUser } from "reactfire"; const firebaseConfig = { apiKey: "...", authDomain: "...", databaseURL: "...", projectId: "...", storageBucket: "...", messagingSenderId: "...", appId: "..." }; const Login = () => { const auth = useAuth(); const [email, setEmail] = useState(""); const [pass, setPass] = useState(""); const [error, setError] = useState(null); const [pending, setPending] = useState(false); const mounted = useRef(true); useEffect(() => { const cleanup = () => { mounted.current = false; }; return cleanup; }, []); const onSubmit = async e => { e.preventDefault(); setError(null); setPending(true); try { await auth().signInWithEmailAndPassword(email, pass); } catch (e) { console.log(e.message, mounted); if (mounted.current) setError(e); } finally { if (mounted.current) setPending(false); } }; return ( <div> <form onSubmit={onSubmit}> <input type="email" value={email} onChange={e => setEmail(e.target.value)} placeholder="Email..." /> <input type="password" value={pass} onChange={e => setPass(e.target.value)} placeholder="Password..." /> <button type="submit">Login</button> {pending && "Pending..."} {error && `Error: ${error.message}`} </form> </div> ); }; const Logout = () => { const auth = useAuth(); const [pending, setPending] = useState(false); const mounted = useRef(true); useEffect(() => { const cleanup = () => { mounted.current = false; }; return cleanup; }, []); const logout = async () => { setPending(true); await auth().signOut(); if (mounted.current) setPending(false); }; return ( <div> <button type="button" onClick={logout}> Logout </button> {pending && "Pending..."} </div> ); }; const Main = () => { const user = useUser(); if (!user) { return <Login />; } return ( <div> User: {user.email} <Logout /> </div> ); }; const App = () => { return ( <FirebaseAppProvider firebaseConfig={firebaseConfig}> <Suspense fallback={<div>Loading...</div>}> <Main /> </Suspense> </FirebaseAppProvider> ); }; const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);動きました。nextではだめで、canaryを使う必要がありました。
おわりに
結果的には、前回のコードとそれほど変わっていないのですが、
useUser
があるので、自分でcontextを用意しなくていい分は楽になるかもしれません。Suspenseでローディング処理を書かなくていいのも楽ではあります。
- 投稿日:2019-12-13T12:34:58+09:00
5000兆円の凄さを実感するためのアプリ
この記事はPONOS Advent Calendar 2019の16日目の記事です。
きっかけ
5000兆円欲しいねって話になって、どれぐらいの量になるんだろうと思って調べたら「積み上げたら5万km」という情報が出てきた。
イマイチ実感が沸かない、きっと誰かがシミュレータを作ってるはずだ!だって3年前、つまり平成のネタだしね!
でも検索しても出てこなかった。。。
よし!作ろう!!!完成品
こちらです。
https://ackyla.com/5000chouen.html技術とか
最初はcanvasでもりもり動くものを作ろうかなーとか思ってましたが、時間がないので色々諦めて簡単な作りにしました。
ただhtmlとjavascriptとcssを書いてgithubにおいただけです。
The deep seaの影響を受けました。最後に
5000兆円はすごい!!
5000兆円欲しい!!!
- 投稿日:2019-12-13T11:55:25+09:00
jsで素早さ計算ツールをつくってみた
jsで素早さ計算ツールをつくってみた
ソード・シールドが発売して約1ヶ月がたちましたね。
ランクバトルの方も、1ヶ月という短い期間ですが、
なんとなく環境も固まりつつあるのかなと思います。その理由は登場するポケモンの少なさでしょう。
前作のサン・ムーンは約800体のポケモンが登場しましたが、
ソード・シールドでは半分の400体(内部データ含めず)。その中でもカプ・コケコの未登場や、メガゲンガーが使えなくなったことにより
今まで素早さ調整の基準とされてきた130族のラインが、今作ではさほど重要でなくなりました。
※今作で登場する130族はサンダース、コオリッポ(ナイスフェイス)のみまた、ダイマックス技のダイジェットの登場により、飛行技を覚えるポケモンの素早さを1段階上げることができます。
実際、ギャラドスなんかも以前は竜舞型が多かったですが、今は飛び跳ねるをダイジェットにして素早さを上げる型が大半です。
何が言いたいかというと、ソード・シールドではより多くのポケモンが素早さを手軽に上昇することができ、
それを前提にした素早さ調整が大事になって来るのではないかと思われます。そういった時に、手早く素早さを実数値で確認できるツールがあると便利だなぁと思って、今回JavaScriptで「すばやさ計算ツール」を作ってみることにしました。
う〜ん、むずかしい!!!
会社の人にアドバイスをもらいながら、コツコツとつくりました。
そしてできたのがコチラ↓↓
subayasa.siteソースの中身には自信がありませんが、UIには多少こだわっており、ファーストビューで実数値が確認できたり、努力値を±1づつボタンで変更したりできます。
たぶん、他のサイトにはないUIなので、需要は多少なりともあるとは思います。
今回で一旦完成ですが、バージョンアップしたり、アプリ版も出したいと考えてますので、
その時はまた紹介していこうと思います。
- 投稿日:2019-12-13T11:36:55+09:00
DataTablesを日本語化したときの留意点
DataTablesについて
DataTablesはHTML上で表(テーブル)を扱う時に非常に便利なJavaScriptのライブラリです。
高機能で、必要になりそうな操作はほとんどがプラグイン形式で用意されています。
そのなかで、1点、重要と思われる留意点を見つけましたので共有したいと思います。国際化プラグイン(language.urlオプション)はテーブルの初期化を非同期にする
国際化プラグインを使ってUIの日本語化(国際化)を行うと、設定オブジェクトの取得がAjaxで行われるため、
$(dom).DataTable()
による初期化とそれ以降の処理のタイミングがずれます。
公式ドキュメントにもちゃんと注意書きがありました。Note that when this parameter is set, DataTables' initialisation will be asynchronous due to the Ajax data load. That is to say that the table will not be drawn until the Ajax request as completed. As such, any actions that require the table to have completed its initialisation should be placed into the
initComplete
option callback.つまり、公式ドキュメントで仮に下記のような見本があったとして、
var table = $("#DataTable").DataTable({...});//オプションを渡して初期化 table.xxxApi().doAnythng();//何かのAPIを呼び出してDateTablesを操作する初期化のオプションに
language.url
が含まれていると2行目のAPIの動作は保証されません。
DataTablesを使っていて、APIの挙動がおかしい、イベント処理やDOM制御が上手くいないときは、これを疑ってください。解決策
initCompleteオプションを使う
DataTablesの初期化オプションに用意されている
initComplete
オプションでハンドラを設定して、DataTablesの初期化が完了してから後続の処理を行います。$("#DataTable").DataTable({ "language" : {"url":"any.json"}, "initComplete" : function(settings, json) { this.xxxApi().doAnythng();//ここで処理する } }); //以下、DataTablesとは関係しないその他の処理この場合は
this
でAPIにアクセスできますが、別途、関数を定義して実行する場合には$(dom).DataTable()
と$(dom).dataTable()
の違いにも注意が必要です。
ちなみに、このハンドラではDataTableの設定オブジェクトとテーブル内のデータを受け取ることができますので、それらに応じて諸々の処理が可能です。language.urlを使用しない
素直に
language
オプションにコードからオブジェクトで設定を渡す方法もあります1。日本語の設定はこのページに記述されています。カスタマイズの際にも参考にしましょう。
この場合は実行タイミングのずれがないため、公式ドキュメントの例の通りに記述しても動作してくれるはずです。まとめ
DataTablesは便利で高機能ですが、それだけに動作は複雑です。機能とそれに関わるAPIやオプションは、Google先生の翻訳に頼りながらでも一読しておきましょう。
私は拡張機能のButtonsのDOM操作で数時間悩みました。Buttons自体はとても便利な拡張ですよ。
項目も多いしバージョン変更などがあると対応に追われそうですが… ↩
- 投稿日:2019-12-13T11:09:03+09:00
Salesforce + JavaScript開発者をご機嫌にする JSforce 2.0 α版を速攻プレビュー
ついに出る! JSforce 2.0
JSforce 2.0 のαバージョンがリリースされています。(2.0.0-alpha.1)
2.0は、1.xでの JavaScript (ES5)ベースのコードからTypeScriptでの総書き換えになります。一時期はJSforceのAuthorの好みというか主義からFlowtypeでの書き換えを行っていましたが、昨今のTypeScriptの型表現力の向上と採用数の増加を受けてTypeScriptに途中で書き換えられました。そのため、4,5年くらい前から計画されていた2.0ではありますが、やっとのαリリースになります。
JSforceがTypeScriptベースで書き直されたことの恩恵は、まずはライブラリ内のコードの品質を高める効果が第一となるので、同ライブラリを利用するだけのユーザから見ればあまり感じないかもしれません。しかし昨今ではTypeScriptを用いてアプリケーションを開発するユーザも多いでしょう。その場合、TypeScriptの型定義ファイルが同梱されることによって、より安全にAPIを利用した開発が可能になっています。
α版なのでインストールの際には明示的にバージョン指定が必要です。
$ npm install -g jsforce@2.0.0-alpha.1なおα版につき現在いくつかのAPIモジュール(stream, bulk など)はまだ利用できません。
2.0の注目機能をピックアップ
TypeScriptで項目名を自動補完!スキーマ型機能
JSforce2.0の最大の特徴はTypeScriptで書き直されたことで型定義がネイティブに提供されることです。
もちろん今までもDefinitelyTypedで有志によって公開された型定義ファイルを入手することで型を使った開発はできましたが、一部APIインターフェースと型定義が異なるところがあったり、ざっくりとしか定義されていなかったりと、少し不満もあったのではないかと思います。またSalesforceオブジェクト/項目などのスキーマについては未対応であるなど、Apexでのスキーマ情報を活かした開発に慣れている人にとっては物足りない面もありました。2.0ではTypeScriptの型推論能力を活用し、APIのインターフェースのみならず、クエリやCRUD操作においてSalesforceオブジェクトのスキーマ情報を利用したレコードの型チェック/推論ができるようになりました。これにより、Visual Studio Codeなどを利用してコーディングを行う際、
SObject#find()
などのクエリで絞り込む項目名や取得する項目名を自動補完したり、クエリ実行後に取得されたレコードに含まれる項目の型をTypeScriptで保証できるようになります。項目自動補完がどのように行われるのかはこちらのスクリーンキャストをご覧いただければ分かるかと思います。
上記動画の完成コードは以下のようになります。
import jsforce, { Connection, StandardSchema } from 'jsforce'; (async () => { // Specify schema of connecting organization const conn = new Connection<StandardSchema>(); await conn.login(process.env.SF_USERNAME, process.env.SF_PASSWORD); // fetch records using find() const recs = await conn.sobject('Opportunity').find({ CloseDate: { $lte: jsforce.Date.YESTERDAY }, IsClosed: true }); // output retrieved records info for (const rec of recs) { console.log(rec.Amount, rec.Name, rec.LastActivityDate); } })();Connectionインスタンスの作成時のコンストラクタ呼び出しの型変数として
StandardSchema
というスキーマ用の型を指定しています。こちらは標準オブジェクト/項目のみを利用する場合のビルトインのスキーマ型定義です。もし自分の開発組織でカスタムオブジェクト/項目を追加している場合は、カスタムのスキーマ型定義を生成して利用することができます。組織に接続してスキーマ型定義ファイルをダンプするためのコマンド
jsforce-gen-schema
が新たに用意されています。$ jsforce-gen-schema --help Usage: jsforce-gen-schema [options] Options: -u, --username [username] Salesforce username -p, --password [password] Salesforce password (and security token, if available) -c, --connection [connection] Connection name stored in connection registry -l, --loginUrl [loginUrl] Salesforce login url -n, --schemaName [schemaName] Name of schema type (default: "MySchema") -o, --outputFile <outputFile> Generated schema file path (default: "./schema.d.ts") --sandbox Login to Salesforce sandbox --no-cache Do not generate cache file for described result in tmp directory --clearCache Clear all existing described cache files -V, --version output the version number -h, --help output usage information $ jsforce-gen-schema -u username@example.org -p pass123 -n MySchema -o ./myschema.d.ts Logged in as : username@example.org describing global describing AcceptedEventRelation describing Account describing AccountBrand describing AccountBrandShare describing AccountChangeEvent describing AccountCleanInfo ... Dumped to: ./myschema.d.ts利用時には生成された
myschema.d.ts
ファイルをimportして、スキーマ型をコンストラクタの型変数に指定します。import jsforce, { Connection } from 'jsforce'; import { MySchema } from './myschema'; ... const conn = new Connection<MySchema>();ログインの必要なし!Salesforce CLIの接続を利用できる接続レジストリ機能
今まで正式にはドキュメント化されていませんでしたが、JSforceには接続レジストリという機能がありました。これはJSforce CLIで接続した接続済のコネクションをJavaScriptコード内から名前指定で利用できるという機能です。取得した
Connection
インスタンスはすでに接続済みなので、ユーザ名/パスワードやOAuthなどでログイン処理を書く必要がありません。import jsforce from 'jsforce'; (async() => { const username = 'username@example.com'; const conn = await jsforce.registry.getConnection(username); const accounts = await conn.sobject('Account').find().limit(10); console.log(accounts); })();今回、この接続情報のレジストリに、Salesforce CLI(sfdx)が保管する組織の接続情報を利用できるようになりました。
Registry#getConnection()
にはユーザ名だけでなくエイリアスも指定可能です。index.jsimport jsforce from 'jsforce'; (async() => { const usernameOrAlias = 'username@example.com'; // const usernameOrAlias = 'my-scratch-org'; const conn = await jsforce.registry.getConnection(usernameOrAlias); const accounts = await conn.sobject('Account').find().limit(10); console.log(accounts); })();なおsfdxで保持されている組織の接続情報をレジストリとして用いる場合は、現在実行時に環境変数
JSFORCE_CONNECTION_REGISTRY
にsfdx
と指定しておく必要があります。$ export JSFORCE_CONNECTION_REGISTRY=sfdx $ node index.jsなお、JavaScriptプログラムからだけではなく、JSforceのREPLでもsfdxレジストリを利用することができます。Scratch組織に接続してゴニョゴニョしたいときにとても便利です。
$ export JSFORCE_CONNECTION_REGISTRY=sfdx $ jsforce -c MyScratchOrg Logged in as : test-abcdefghijkl@example.com > query('SELECT count() FROM Account') { totalSize: 78, done: true, records: [] }Promiseスタイルの呼び出しがデフォルトに。Callbackスタイルのコードは書き換え必要(かも?)
JavaScript(およびNode.js)での非同期関数呼び出しの慣習として、コールバック関数を非同期関数の末尾引数として渡し、非同期の返値(およびエラー)をそのコールバック関数の引数で受け取る形式が主流でした。しかしながら、ES6(ES2015)以降にPromiseがJavaScriptでも標準化されたことで、PromiseでのAPIコールが2.0ではデフォルトとなっています(なお1.xでもPromise形式でのAPIコールはサポートされています)。
Promiseは従来の
Promise#then()
によるチェーン呼び出しで非同期処理を連結しますが、ES2017より導入されたasync
/await
を利用するのがよりモダンな書き方でしょう。async function main() { const conn = new jsforce.Connection(); const { id: userId } = await conn.login( 'username@example.org', 'password123' ); const recs = await conn .sobject('Account') .find() .limit(10); const urecs = recs.map(r => ({ Id: r.Id, OwnerId: userId })); const rets = await conn.sobject('Account').update(urecs); for (const ret of rets) { if (ret.success) { console.log(ret.id); } else { console.error(ret.errors.map(e => e.message).join('\n')); } } }現在の2.0αでは、コールバック形式の呼び出しはサポートされていません。正式リリースの際にコールバック形式が残されるかどうかは現時点では不明です。
Semantic Versioningに正式対応(予定)
今までJSforceのversionは、実はSemantic Versioningにはなっていませんでした。例えば1.3から1.4になる際に、
Metadata.create()
のAPIインターフェースは変わっているのですが、メジャーバージョンを上げていません。2.0リリース以降はインターフェースの変更についてはメジャーバージョン(2.0 => 3.0)で対応することになります。まとめ
以上、まだalphaバージョンですが、coreの実装は固まりつつあるので、一度触ってみてフィードバックいただければと思います(特にTypeScriptユーザにぜひ)。
なお、2020年1月のJapan Dreamin' にてJSforce 2.0が初お目見え?するらしいです。Authorから直接JSforceの内容をセッション形式で聞けるのはグローバル含めて初とのことなので、参加してみてはいかがでしょうか。