20190324のJavaScriptに関する記事は23件です。

目隠しパターンを生成するスクリプトを書いてみた

他人に読まれたくない紙を捨てたい

他人に読まれたくない情報が書かれた紙を廃棄するのに目隠しパターンが出るコロコロスタンプを使用しているのですが、大量に紙を処分したいときに、スタンプのインクが切れたら嫌だと思い、インクジェットプリンターで紙一面目隠しパターンで塗りつぶすスクリプトを考えてみた。

仕組み

稚拙なスクリプトですが、ランダムな英数字をランダムな方向、位置で書きつぶすJavascriptを書いてみました。
このhtmlをブラウザで開いて目隠ししたい紙をプリンタにセットして1枚に収まるように印刷すればいいはず…

<html>
<body>
  <svg id="picture"></svg>

  <script>
    const draw = () => {
      const dens=3.5;
      const h=297*dens;
      const w=210*dens;
      const n=700*dens*dens;
      const color="black";
      const c = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789";
      const box=document.getElementById("picture");
      box.setAttribute("width", w);
      box.setAttribute("height", h);
      box.setAttribute("stroke", color);
      for(let i=0; i<n; i++){
        const letter=document.createElementNS("http://www.w3.org/2000/svg", "text");
        letter.setAttribute("x", Math.random()*w);
        letter.setAttribute("y", Math.random()*h);
        letter.setAttribute("rotate", Math.random()*360);
        letter.textContent=c[Math.floor(Math.random()*c.length)];
        box.appendChild(letter);
      }
    };
    draw();
  </script>
</body>
</html>

課題

  • FireFoxで印刷しようとすると、余白とか用紙サイズとかがぴったりいかない。
  • 色を変えたいときにスクリプトを触る必要がある。

気が向いたら更新してゆきます。

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

npm initの-fオプションについて解説。どう変わる?

こんにちは。俳句や川柳から動くGIF画像を生成できるWebサービス「五七五メーカー」をリリースしたアカネヤ(@ToshioAkaneya)です。
npm initの-fオプションについて解説します。

npm initの-fオプションについて解説

たまに、技術記事などで
$ npm init -fを実行して下さい
と書かれているのを見たことがあると思います。(僕も時々使います。)
これは、通常$ npm initした時に出てくる質問に対して、全てエンターを押した場合と同じ様な動作をするコマンドになります。
npm initした後の質問に全てエンターを押すつもりの場合は、$ npm init -fを実行してね、と伝えた方が楽なのでよく使われます。
以上です。

はてなブックマーク・Pocketはこちらから

はてなブックマークに追加
Pocketに追加

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

さくら前線を可視化してみる

さくら前線を可視化してみる

さくらの季節になり仕事も少し落ち着いたので、土日を利用して、気象庁生物季節観測の公開データからさくら前線の可視化をしてみました。

気象庁生物季節観測とは

気象庁では、毎年、さくらの開花日や満開日やかえでの紅葉日、つばめの初見日など、季節を代表する生物の動向が観察されています。近年は地球温暖化の影響か、さくらの開花日等が早まっているという話もありますが、生物の生活史の変化を観察することで気候変動等を観測するもの理解しています。

作った理由

別途、地球温暖化対策を計画してみようというサイトを作っているのですが、地球温暖化や生物多様性に関心があり、気候変動が生物の生活史にどんな影響を与えているか、ちょっと可視化してみたくなりました。。

作成方針

1.ライブラリ等
当初、processing.jsp5.jsで作りたかったのですが、地理情報処理(座標変換)関係で挫けたので、結局OpenLayersで作成しました。サーバーサイドはSpark Frameworkを使用しました。
2.可視化手法
生物季節の観測値は日付データなので、毎年1月1日を起算日とする経過日数データとし、その等高線でさくらの開花・満開の前線を描画しました。等高線描画手法は、観測地点を節点とする非構造格子をデローニ分割で生成し、各三角要素を検査して描画するアルゴリズムとしました。なお、デローニ分割については数年前に一度投稿したdelaunay.jsを書き直して使用しました。

成果物

作成したものは、Githubにリポジトリを登録し、Herokuにデプロイしました。
■生物季節可視化サイト:https://phenologicalmapjp.herokuapp.com/
■Githubリポジトリ:https://github.com/termat/PhenologicalMap
■動画

経年比較データをみると、やはり最近、開花日、満開日が早まっている様子が見て取れます。

雑感

等高線データはGeoJSON形式で生成し、Openlayersに読み込ませています。その関係で可視化としてはイマイチな感じになってしまった。

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

3歳娘「パパ、関数をカリー化して?」

再就職する前のワイ

ワイ「あー、今日は休みやな」
ワイ「いうて昨日も一昨日も休みやったけど」

娘(3歳)「パパ、お絵かきしよ」

ワイ「ええで」

娘「あれ描いて。あの・・・アンパンマ(n

ワイ「おっとそこまでや
ワイ「著作権のあるものを言うたらアカン」
ワイ「あれやな?アンパンパーソンのことやな?」
ワイ「描いたるで」
ワイ「チョイチョイチョイ、と」
ワイ「こうやな」

娘「わあ。上手。」
娘「次は食パンマ(n

ワイ「食パンパーソンやな?」
ワイ「描いたる描いたる」
ワイ「チョイチョイチョイ、と」
ワイ「こうや」
ワイ「ついでにバイキンパーソンも描いたる」

娘「すごいね。上手だね。」
娘「じゃあ、次は・・・」

娘「カリー化された関数を書いて

ワイ「(おっと?)」
ワイ「カリー・・・カレーパンパーソンのことかいな?」

娘「ちがう、カリー化された関数書いて」

ワイ「お、おう」
ワイ「分かるで〜
ワイ「カリー化された関数が欲しい年頃やもんな」

よめ太郎「(分かるんかい)」
よめ太郎「(どんな話の合わせ方や・・・)」

ワイ「ええけど、そんなもん何に使うんや」

娘「カリー化された関数を部分適用して」
娘「3を足すだけの関数を作ってほしいの」

ワイ「え、ええ・・・?」

娘「みんな持ってるの
娘「私だけ持ってないの

ワイ「(いや、そのセリフはおねだりの常套句やけど)」
ワイ「(この文脈では無茶やろ・・・)」
ワイ「(いや、最近の子はホンマにみんな関数を持ってるんか・・・?)」
ワイ「(・・・まあ、娘ちゃんが喜ぶならどっちでもええか・・・)」

ワイ「わ、分かるで〜
ワイ「ワイが子供の頃も3を足すだけの関数が流行ったもんや

よめ太郎「(分かり過ぎやろ)」
よめ太郎「(もはや何でも分かる人になってるやん)」
よめ太郎「(ていうかもう、筆者が書きたいことを書くためにストーリーむちゃくちゃなってるやん・・・)」

ワイ「さあ、関数をカリー化していくで〜」

カリー化とは

ワイ「たしか、カリー化いうんは」

function tashizann(hikisu1, hikisu2) {
    return hikisu1 + hikisu2;
}

ワイ「例えばこういう普通の足し算関数を」

tashizann(3, 5);

ワイ「こういう風に2つの引数を渡すんやなくて」

tashizann(3)(5);

ワイ「こういう風に1つずつ引数を渡す関数に変換することやったな」
ワイ「そうするには・・・」
ワイ「どないすればええんや」

ワイ「要はtashizann(3)の部分が、戻り値として関数を返してくれれば
ワイ「返ってきたその関数に、引数として5をぶち込んで」
ワイ「実行してやればええねんな」
ワイ「ということは・・・」

function tashizann(hikisu1) {

}

ワイ「こんな感じで」
ワイ「tashizannが最初に受け取る引数は1つだけ
ワイ「今回の例で言えば3だけを受け取るようにする、と」
ワイ「ほんで」

function tashizann(hikisu1) {
    return function(){

    }
}

ワイ「こうや」
ワイ「tashizannは戻り値として関数を返すんや・・・」

ワイ「ほんで、その返した関数5をぶち込みたいから」
ワイ「内側の関数にも引数を1つ指定せなあかんのやな」

function tashizann(hikisu1) {
    return function(hikisu2){

    }
}

ワイ「こうや」
ワイ「これでhikisu25が入ってくるはずや」

ワイ「ほんで、その内側の関数最終的に返す値は」
ワイ「3 + 5やから・・・」
ワイ「return hikisu1 + hikisu2;ってしてやればええんやな?」

function tashizann(hikisu1) {
    return function(hikisu2){
        return hikisu1 + hikisu2;
    }
}

ワイ「こうやな」

ワイ「試してみよか」

const kekka = tashizann(3)(5);

alert(kekka);

アラート結果:
8

ワイ「おお」
ワイ「とりあえず、2つの引数を受け取るtashizann関数を」
ワイ「1つの引数を受け取る関数ネストしたものに変換できたで」

Next: 部分適用

ワイ「カリー化は出来たから、次は部分適用や」
ワイ「さっきカリー化した関数を使って、3を足すだけの関数を生成すればええんやな」
ワイ「さっきのtashizannという関数に3だけを渡すと」
ワイ「関数が返ってくるはずやから」
ワイ「その返ってきた関数定数に入れて保存しとけばええんや」

const tasu3 = tashizann(3);

ワイ「こうやな」
ワイ「これで、tashizann部分適用してtasu3いう関数を作ることが出来たはずやで」

ワイ「実際にtasu3を使って、試してみよか」

const kekka2 = tasu3(5);

alert(kekka2);

アラート結果:
8

ワイ「おお、できたで」

娘(3歳)「わーい!」
娘「これで3を足し放題だ〜!!!」

ワイ「・・・ホンマにこんなもんが欲しかったんか?」

娘「何言ってるの」
娘「実務でもけっこう使えるよ?」

ワイ「(実務!?)」
ワイ「(義務教育もまだやのにもう実務を視野に・・・!?)」
ワイ「(末恐ろしい子やな・・・)」

娘「HaskellElmなんかだと、関数がデフォルトでカリー化されてるんだから」

ワイ「せ、せやな(知らんけど)」

そんなこんなで晩ごはん

ワイ「いただきますやで〜」
ワイ「はぁ、明日から新しい職場で働くんやな〜」
ワイ「ワイはちゃんとやっていけるんやろか」
ワイ「同僚のみんなのレベルについていけるんやろか」
ワイ「毎日プログラミングのお勉強はしとるけど、それでもまだ不安や・・・」

娘「パパ」
娘「前もって先のことを考えたり、心配しておくのは」
娘「基本的には良いことだと思うけど」
娘「それで考えすぎて心が疲れちゃうくらいなら」
娘「いったん考えるのをやめて、今このご飯が美味しいってことにフォーカスを当ててみよ?」

娘「まだ来てない明日のことで傷つくより、」
娘「目の前にあるご飯の美味しさを、集中して味わおうよ」
娘「そのほうが心も安らいで、むしろ明日の力になるんだよ」

ワイ「せやな・・・」
ワイ「ありがとう娘ちゃん・・・」
ワイ「ウマい・・・ウマいで・・・」

よめ太郎「(できた子や・・・)」
よめ太郎「(ワイの血やな・・・)」

〜おしまい〜

追記1: アロー関数式

アロー関数式を使って書くとここまでシンプルにできるで。

const tashizann = hikisu1 => hikisu2 => hikisu1 + hikisu2;

追記2: カリー化や部分適用のメリット

例えば

const styleChange = prop => value => elm => elm.style[prop] = value;

こんな関数を作っておいて、

document.querySelectorAll(".className").forEach(styleChange("color")("red"));

こんな感じで即席関数を作ってforEachメソッドに渡せたり・・・とかですかね。
ぶっちゃけ使い所はあんまり分かってません・・・。

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

setIntervalより正確な時計を作る

setIntervalは手軽ですが、それが故に正確な時間を必要とする場合には、あまり役に立たなくなります。

手軽に時計を作ってみる

setInterval(関数, 1000);のように書けば、関数を1秒毎に実行することとなります。ここで描画処理を入れれば、秒単位で表示する時計の一丁上がりです。

…が、このままではうまくいかない箇所があります。

  • スタートのタイミングの問題…setIntervalをスタートする瞬間がちょうど秒の変わる瞬間だったという幸運なパターンもありえますが、最悪の場合1秒近く遅れることとなります。
  • 1秒より間隔が長くなる…setIntervalの時間は、「前の実行が終わってから次の実行が始まるまでに、最低限確保される待ち時間」ですので、実行同士の間隔は関数自体の実行時間が足し込まれて、必ずセットした時間を上回ることになります。また、他のコードの実行で遅れることもありえますし、ブラウザ側で意図的に(消費電力節約のためなどで)それ以上待たせてもいいことになっています(W3C HTML 5.2)。
    • 上の2つが重なれば、「遅れがそこまででもないのに秒が飛ぶ」ことも考えられます。1回めを0.999秒で実行開始したとして、タイマーが2ミリ秒余分にかかれば、次の実行は2.001秒となって、1秒台の実行がなくなってしまいます1

このような事情がありますので、「できるだけ安定したサイクルを保ちたい」あるいは「秒が変わった瞬間に実行したい」という場合、setIntervalはあまり適当な選択肢ではありません。

setTimeoutを使う

では、代わりにsetTimeoutを使ってみましょう。setTimeoutでループ処理をさせるときの基本形は、以下のような形です。タイマーで呼ばれた関数から、さらに同じ関数をsetTimeoutする、という流れです。

setTimeout(function main(){
  // メインの処理

  setTimeout(main, 時間);
}, 時間);

あとは時間をどう設定するかだけですが、Date.now()で現在のミリ秒が得られます。「ちょうど秒が変わる瞬間」はシリアル値が1000で割り切れますので、1000 - Date.now() % 1000のようにすれば、そこまでの待ち時間を導けます。

毎回待ち時間を正しく設定することで、秒の変わり目にできるだけ近いところでコンスタントに関数を実行できますし、処理の遅延が1秒以上にならない限り、秒が飛ぶことも起きません。

Codepen上に作ってみました

リロードなどでタイミングを調節すれば、setIntervalのほうが遅れている様子もわかるかと思います。

(なぜかReactで作ってしまったので、ちょっと見づらいかもしれません)

See the Pen pYGKgR by Jkr2255 (@jkr2255) on CodePen.


  1. もちろん、ブラウザ処理の重さなどで、実行が遅れうることは予め想定して実装が必要なのは間違いないです。 

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

コールバック関数が何回やっても覚えられなかった自分用に記事を書きたかった

JavaScriptを学んでいる途中でコールバック関数にドハマりしたので自分用に記事を書きました。

そもそもコールバック関数とは

コールバック関数とは、「関数の引数として関数を渡すこと」です。



そもそもこの文章の意味が分からない

そう、JavaScript初心者には、この文章ですら意味不明。

とりあえず分かったのは、例えば関数Bを、関数Aのあとで、しかも確実に実行したい場合などに使われる方法であるということです。
様々な理由により、関数Bの方が先に実行されてしまい、不具合を起こしてしまうケースというのが存在する、ということでした。

検索しても分からない

どういうときに使うのかは分かった、しかしどう動いているのかは未だに分からない。
こういう時に頼りになるのは先人の知恵。

という訳で早速「コールバック関数」で検索をしたものの、これも全く分からない

「ねっ?簡単でしょ?」と言わんばかりにどの記事もコードが書いてはあるものの、「そうじゃない!もっと簡単なところから教えてください!(泣)」という私の個人的なニーズに応えられる記事は見つからず。

最終的には何となく分かったけど、もっと簡単に書いてある記事があっても良いのでは…
自分みたいにJavaScriptを勉強し始めた人の中にも、コールバック関数で躓いている人がいるはず…いや、いてほしい

そんな中で自分なりに考えて辿り着いた、理解の方法がこの記事になります。
多分、これが一番簡単だと思います。

まずは「引数」をおさらいしよう

自分がそうだったので書いておきますが、コールバック関数が理解できない人は「引数」がちゃんとは分かっていないのかも知れません。
例えばこんな簡単な関数を考えてみましょう。

function show(x) {
  console.log(x);
}

この関数ではxが「引数」なので、この"x"の部分に任意の値を入れて実行すれば、その値がconsole.logに表示されます。
例えば下記の様な形です。

function show(x) {
  console.log(x);
}

show(y); //console.log(y);
show(z); //console.log(z);

show()という関数を実行する際に、「引数」に渡されたxconsole.logで返される。
同じくyならyzならz...と、渡された引数の値がconsole.logで返されます。

じゃぁこれは?

それでは、このコードの2行目を下記の様に書き換えてみるとどうでしょうか?

function show(x) {
  x();
}

console.logが消えて、これまで「引数」だったxが前に出て、「x()という関数を実行せよ」というプログラムに変わりました。
つまり、関数show()に渡された「引数」に従って、その「引数」の名前の関数が呼び出されることになります。
挙動としては下記です。

function show(x) {
  x();
}

show(y); //y(); ...y()という関数が実行される
show(z); //z(); ...z()という関数が実行される

コールバック関数の実践

ここまで理解できれば次のプログラムがどう構成されているかも分かると思います。

//3.
function y() {
  console.log("call back!");
}

//2.
function show(x) {
  console.log("call back soon...") 
  x();
}

//1.
show(y);

  1. まず初めに、show(y);という関数が実行され、console.log("call back soon...")に基づき、コンソールに表示されます。
  2. 次に、x();という関数が実行されますが、このxは引数によってyに置き換えられるため、実際にはy();という関数が実行されます。
  3. そして、y();という関数は既に定義してあるので、console.log("call back!")が実行されます。

他の記述方法

なお、関数を変数に代入する形で、下記の様に記述して紹介している場合もあります。
↑に書いたコードと同じ意味なんですが、コールバック関数が理解出来なくて混乱している時に読むとますます意味が分からなくなると思うので、注意しましょう。

var y = function() {
  console.log("call back!");
}

var show = function(x) {
  console.log("call back soon...") 
  x();
}

show(y);

まとめ

細かいところをすっ飛ばしての説明となりましたが、多分こう考えればコールバック関数も理解しやすくなると思います。
コールバック関数でドハマりしている人が抜け出すために、少しでもお力になれれば幸いです。

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

[翻訳]JavaSciptプロジェクトでいずれは学ぶ10のこと

以下は、The Cat with a Dragon Tattoo によって2018/8/3にMediumに投稿され、現在1万Clap以上受けている記事、"10 Things You Will Eventually Learn About JavaScript Projects"=「JavaSciptプロジェクトでいずれは学ぶ10のこと」の日本語訳です。

特定のライブラリを超えた、しかしフロントエンド開発者を主眼とした学びがまとまっており、僕がまさに読みたかったものでした。著者に感謝を。

表示 - 非営利 - 継承 4.0 国際 (CC BY-NC-SA 4.0)
https://creativecommons.org/licenses/by-nc-sa/4.0/deed.ja


JavaScriptは冒険です。
様々な業界で10年近く開発してきましたが、誰だってこれには同意してくれると思います。

フロントエンド開発は、プログラマーである私たちに、多くの選択の自由、柔軟性、創造の余地を与えてくれます。しかし代わりに、ちょっとした知識、計画、責任が求められます。

jQuery、require.js、Angular、React、ExtJs、その他思い出せない(または思い出したくない)何十ものプロジェクトを経験してきましたが、想像もしなかった酷いモノを見てきました。きっと皆さんもそうでしょう。

しかしどんな時代でも、まとまりのないプロジェクトをどうにか扱いやすく変えられる「共通のパターン」がありました。以下では、その中でも特に重要な10のパターンを記します。個人的な経験から導いた意見ですが、経験豊富なエンジニアの多くは頷いてくれるのではないでしょうか。これらのパターンは、どんなフレームワーク・方法論・チームの規模であっても、ドキュメンテーションやリファクタリングの必要性を減らし、エンジニアの目から零れる涙を減らし、プロジェクトの盤石な基礎となってくれるでしょう。

あなたに新しい発見があり、これらのパターンを便利だと感じ、またこれらを使って素晴らしい何かを作ってくれることを願います!:muscle:

1.分割統治

聞いたことはありながら、多くの人がこのルールを過小評価しています。CommonJS、Webpack、Nodeではコードを複数のファイルに分割できます。でも、何故わざわざそんな事をするのでしょう?

一貫性

プロジェクトを1つのexportを持つファイルに分割れば、プロジェクトが大きくなった時、検索や依存性の管理がずっと簡単になります。「ファイルの名前はそれがexportするものを示す」というルールは直感的で、脳みそを疲弊させずにアーキテクチャを概観することができます。

管理

各exportを独立ファイルに分割することで、必要なときに素早く移動できるようになり、分業が促進されます。ヘルパー関数がアプリケーションの他の場所で必要になれば、/sharedフォルダを作ってファイルを移動すれば良いわけです。そうすれば、また他の部分からも参照できます。

2.恥ずかしいくらい明確に

変数、関数、ファイルの名前は、自分の子供の名前だと思って時間をかけて決めましょう。変数xとすれば今日の0.3秒が節約できるかもしれませんが、1ヶ月後には2日かけて意味を解明し、4日かけてリファクタすることになります。先々のことを考え、長い名前を恐れないようにしましょう。

ハック的な手法や、MITに入学したくなるような難しいやり方は避けましょう。その解決法は本当にスマートなのかもしれません。未来のあなたやチームメートも、コードの解読にたっぷり時間を費やした後で「賢いね!」と同意するかもしれません。しかし、物事をシンプルに保つことに集中し、なるべくドキュメントやコメントが必要ないようなコードを目指しましょう。

3.マジックナンバーやマジック文字列を解消する

どんなにやりたくなっても、マジックナンバー・マジック文字列は使ってはいけません。小さなもの、さして重要でないものでも、意味ある名前の変数に格納して、スコープの冒頭に置きましょう。

明示的にコードに書いた値は多くの場合どこかで再利用されます。すぐに変数に格納すれば、コードの重複は減り、修正は楽になり、値に意味を与えられます。

4.ネストと戦う

コードが横に120文字/縦に500行以上になるかif文が3層以上になったら、全力で分割しましょう。

深くネストされたif文を別々の関数、Promise、Observableに分割することで、条件文の複雑性を解消することができます。非同期呼び出しが多いなら、async/awaitも大幅にコードをシンプルにしてくれます。

5.configを頑張る

グローバル変数、APIエンドポイント、フィーチャートグル、サードパティーの認証情報を使うなら、分離したconfigファイルに置きましょう。

WebでもNodeでも、config管理を助けてくれるconfigのようなパッケージが多数あります。あなたのアプリケーションはある時点でサーバーとローカル開発環境の両方で動くことになります。configファイルの作成は遅いより早いほうが簡単で、各環境ごとに挙動・使う認証情報・その環境で動く機能などなどを調整することができます。

6.フレームワークはあなたを助けるためにある

知っているから、人気だからという理由でフレームワークが使われている事がよくあります。

自分のプロジェクトにフレームワークが必要なのか、必要であればどれなのか、じっくり考えましょう。あなたのサイトがGitHubで10万starを獲得したフレームワークを活用していようがいまいが、エンドユーザーにとっては全くどうでも良いことです。
経験から、私はフレームワークを以下のように分類しています。

React

コンポーネントベースのWebアプリケーションで、アーキテクチャを完全にコントロールしたい場合に使えます。Reactエコシステムでの開発は、時間もかかり、事前に十分な計画が必要です。十分な知識がある場合に限り、Reactは多くのメリットをもたらします。

Angular / VueJS / Ember

信頼性の高いWebアプリケーションを素早く作りたい場合に使えます。引き換えに大きなブラックボックスを抱えることになります。これらのフレームワークは多くの仕事をしてくれるので、自前アーキテクチャの利点・欠点の両方がなくなります。構造が厳しく決められているため、多少間違いをおかしてもReactほど困ることは少ないでしょう。

jQuery / lodash / その類型

Webページを素早く作りたくて、何キロバイトかファイルが増えても良い場合に使えます。開発時間を劇的に短縮してくれますが、メンテナンス不能なコードも書けてしまうので注意が必要です。

Vanilla JS / フレームワークなし

WebページでもWebアプリケーションでも、開発と計画に多くの時間が使える場合に使えます。ピュアなJavaScriptは、実験的なこと...WebGL,Worker,深い最適化,ブラウザアニメーション...を行うのに合っています。最後には自分が新しいフレームワークを作ることになるでしょう。トランスパイラを使えば、よく良く軽量なjQueryの代替物にもなります。

このリストは1つの提案として受け取ってください。フレームワークを使うか、何を使うか、プロジェクトにとってベストな選択をじっくり考えましょう。

7.プロトタイプでないなら、テストを書く

単体テスト、スモークテスト、EtoEテスト、ざっとした確認...。
プロジェクトがすぐに書き換えられるプロトタイプで無い限り、テストを書くべきです。コードベースが複雑になるにつれて、管理・メンテナンスは大仕事になります。テストはそれを肩代わりしてくれます。

未来のいつか、あなたはバグに遭遇し、青空を見上げて、テストを書いてくれた過去の自分に感謝することになります。新機能を追加したときに色々な所をひっそり壊してしまったことを、テストがなければ気づかなかっただろうからです。

8.バージョン管理システムを使う

プロトタイプだろうが、フルスケールの企業Webアプリケーションだろうが、小さな楽しい趣味プロジェクトだろうが、1行目のコードを書く瞬間からgitなどのバージョン管理システムを使いましょう。毎日コミットし、ブランチを使い、マージ・競合の解消・コミットの巻き戻しを学びましょう。意味のあるコミットメッセージを残しましょう。

バージョン管理によって、時間を移動し、壊したものを直し、過去の変更を見ることができます。この記事から1つだけ覚えてもらえるなら、日々バージョン管理システムの基本を学ぶこと、です。なぜなら、記事の残りの部分を無視して間違った道に進んでも、バージョン管理をしていれば直せるからです!それがなければ最初からやり直す運命になります。

9.確実な状態管理

状態管理のデザインパターンかライブラリを探して、自分の命がかかっているかのように齧りつきましょう。本当に命がかかる日がくるかもしれません。

フロントエンドエンジニアとして、通常私たちの大きな挑戦は2つだけです。データを表示することと、保管することです。長期間メンテナンスするという点では保管のほうが遥かに大変で、これを無視して開発するのと楽ができます。そして、数カ月後にプロジェクトは事実上メンテナンス不可能になるでしょう。

データの保管、すなわち状態管理は難しいものです。アプリケーションでは普通、サーバーがデータベースに持つデータとクライアントの画面に表示される内容が同期している必要があります。私たちのゴールは、その中間に置くJavaScriptで余計な複雑性を加えないことです。コンポーネントはサーバーと同じデータを提供し、ユーザーによる変更を同期させ、サーバー側の変更に追従しなければなりません。どうすればこれを実現できるでしょう?

React

Reactはとてもオープンなエコシステムを持っているため、ソリューションも十分にあります。FluxアーキテクチャにはRedux、observableベースならMobXがあります。それぞれメリット・デメリットがあります。使い始める前に各ライブラリの基礎を理解するようにしましょう。

Angular、Ember、VueJS

これらはビルドインで専用の状態管理の仕組みを持っており、コンセプトはObservableを基礎にしています。必須ではありませんが、追加のライブラリとしてngRx、Akita、Vuexがあります。

他のフレームワーク、Pure JavaScript

Redux、MobX、または独自のステート管理の仕組みを使えます。主なゴールは、アプリケーション全体で1つの「source of truth」(信頼できる情報源)を持つことです。この「情報源」は、サービスでも、ライブラリでも、単純なObservableでもありえます。

10.流行を疑う

最後に、コミュニティーに耳を傾け学びつつも、全ての記事、コメント、Mediumへの猫の長い投稿(訳注:この記事。著者のハンドルが猫)、あなたのコードへのフィードバックはよく吟味し、疑ってかかりましょう。フロントエンドの世界ではたくさんの新しいアイデアが勢いよく登場してきます。これらにはオープンな態度でありつつも、お祭り騒ぎを「追いかけるために追いかける」ことのないように気をつけましょう。こうした態度のために忘れさられてしまったプロジェクトが山ほどあります。

古く成熟したフレームワークで書かれたプロジェクトが、新しいものが出たからといって2つのフレームワークを混ぜたようなプロジェクトよりはるかに優れていることは珍しくありません。新しい流行の何かがアプリケーションや開発効率を少し向上させるとしても、一貫性には敵いません。メンテナンス性を保つために自分の最初の選択を守り、必要なときにだけ流行を取り入れましょう。


以上です!
ここまで読んでくれてありがとう。コメントであなたの意見や話を聞かせてくれるのを楽しみにしています。上述の通り、これらは私がJavaSriptとフロントエンド開発に関わる中で得られた一番大事な経験に過ぎません。私たちが経験する挑戦の大海のほんの一滴です。

これらのパターンがあなたの役に立ちますように。

(訳終)

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

pm2で1時間に1回再起動させる

Raspberry Piで実行してますが、不安定なプロセスがあって定期的に再起動出来たらと思ったやつです。

プロセス1を永続化

pm2 start process1.js

ここは通常の使い方。

プロセス1を監視する

npm init -y
npm i pm2

https://github.com/Unitech/pm2/blob/master/examples/api-pm2/api.js

pm2watch.js
'use strict'

const pm2 = require('pm2');
const INTERVAL_TIME = 1000 * 60 * 60; //1時間

const restart = () => {
    pm2.restart('process1', errback => {
        if(errback === null){
            console.log('reloaded');
        }else{
            console.log(errback);
        }
    });
}

setInterval(() => restart, INTERVAL_TIME);
pm2 start pm2watch.js

確認

$ pm2 list

┌────────────┬────┬─────────┬──────┬───────┬────────┬─────────┬────────┬──────┬───────────┬──────┬──────────┐
│ App name   │ id │ version │ mode │ pid   │ status │ restart │ uptime │ cpu  │ mem       │ user │ watching │
├────────────┼────┼─────────┼──────┼───────┼────────┼─────────┼────────┼──────┼───────────┼──────┼──────────┤
│ process1   │ 0  │ N/A     │ fork │ 27236 │ online │ 16      │ 12m    │ 0.2% │ 34.6 MB   │ pi   │ disabled │
│ pm2watch   │ 1  │ 1.0.0   │ fork │ 27701 │ online │ 0       │ 93s    │ 0.2% │ 34.2 MB   │ pi   │ disabled │
└────────────┴────┴─────────┴──────┴───────┴────────┴─────────┴────────┴──────┴───────────┴──────┴──────────┘

定期的に再起動してprocess1の

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

TI SensorTagをブラウザから使う(2):with LINE Things

前回に引き続きTIが販売している小型で電池駆動のSensorTagを使ってみます。
 TI SensorTagをブラウザから使う(1):with Chromeブラウザ

今回は、TIのSensorTagをLINE Thingsに対応させ、LINEアプリの中でセンサー情報を表示してみます。また、ついでに、LINE Beaconにも対応させます。

結構、めんどかったです。SensorTagのファームウェアのカスタマイズが。

以下の手順で進みます。

  • LINE Thingsトライアルプロダクトを作成し、サービス探索用serviceUUIDを取得します。
  • LINE BeaconのためのハードウェアIDを払い出します。
  • サービス探索用serviceUUIDとハードウェアIDを埋め込めるように、SensorTagのファームウェアをカスタマイズします。
  • LINEアプリから接続します。

LINE Thingsのためのサービス探索用serviceUUIDの取得

以下に手順があります。
 https://developers.line.biz/ja/docs/line-things/regist-product/

LINE Things対応デバイスは、LIFFアプリに紐づきます。また、LIFFアプリは、Message APIのチャネルに紐づきます。
そして、LINEアプリユーザは、LINE Things対応デバイスをBLEスキャンして発見し、LINE Things対応デバイス属するチャネルとお友達になることで、LINEアプリからそのLINE Things対応デバイスを扱えるようになります。
LINE Things対応デバイスを扱うためのアプリが、LIFFです。

〇まず、LIFF APP IDを作成します。

LINE Developerページから、Messaging APIのプロバイダを選択します。まだ作成していない場合は作成してください。
そして、LIFFタブを選択します。

image.png

右側にある「+追加」ボタンを押下します。

image.png

名前には適当な名前を付けます。たとえば、「SensorTag Application」とでもしておきます。
サイズは、LIFFアプリのWindowsサイズです。Fullでいいでしょう。
エンドポイントURLには、LIFFアプリのWebページを指定します。1回目の投稿で作成したWebページのことです。
BLE freatureは、当然ONにします。

完了すると、LIFF URLが割り当たります。
line://app/ より右側にある文字列がLIFF APP IDです。

ちなみに、1回目で作成したWebアプリのJavascriptは、LIFFアプリとしても動作するように実装してありますので、LINEアプリからでもChromeからでもどちらからでも開けます。

(参考)
 https://developers.line.biz/ja/reference/line-things/#liff-bluetooth-getavailability

〇次に、サービス探索用serviceUUIDを取得します。

以下のページを参考に、CurlやPostman等を使って取得します。
 https://developers.line.biz/ja/docs/line-things/regist-product/

レスポンスのうちの、「"serviceUuid": "{serviceUuid}"」が大事です。覚えておきます。
忘れてしまったら以下を参考に呼び出せばわかります。
 https://developers.line.biz/ja/reference/line-things/#get-trial-products

ハードウェアIDを取得する。

以下のページから、LINE BeaconのためのハードウェアIDを取得します。

https://admin-official.line.me/beacon/register#/botlist

対象のチャネルを選択して、「ハードウェアID発行」ボタンを押下するだけです。

image.png

LINE Beaconは、この払い出されたハードウェアIDでMessage APIのチャネルに紐づけられます。
LINEアプリでは、LINE Beacon対応デバイスが属するチャネルとお友達になることで、LINE Beaconから通知を受けることができるようになります。

SensorTagのファームウェアをカスタマイズする

変更内容は以下の通りです。これが一番苦戦しました。

  • LINE Things対応デバイス

・ボンディングの前に、サービス探索用serviceUUIDをアドバタイズできる
・ボンディングができる
・ボンディング後の通信が暗号化される
・デバイス特定用characteristicから、デバイスを一意に識別する値を読み込める
・デバイス特定用characteristicは、属性のパーミッションを暗号化(Encryption)に設定してください。
(参考)
 https://developers.line.biz/ja/docs/line-things/create-product/

デバイスを一意に識別する値として、今回はBluetooth MACアドレス(リトルエンディアン)としました。

  • LINE Beacon対応デバイス

・アドバタイズデータに、ハードウェアIDを含む「LINE Simple Beacon Frame」を含める。
(参考)
 https://github.com/line/line-simple-beacon/blob/master/README.ja.md

アドバタイズデータの最大長の制限により、LINE Things対応デバイスとLINE Beacon対応デバイスを同時に有効にすることができませんでした。以降のカスタマイズでは、Characteristicに0x00または0x01を書き込むことによって切り替えられるようにします。
もろもろのソースコード変更分を以下に置きました。オリジナルのファイルも置いておいたので、比較してみてください。

 https://github.com/poruruba/sensortag_modified

ちなみに、コンパイル時にROM容量が足りなくなったので以下の機能を削除しています。

  • OAD(Over the Air Download)
  • FACTORY_IMAGE

ですので、一度更新したらOTAで更新できなくなります。(なので、デバッガボードCC-DEVPACK-DEBUGを所有している方限定なのです)
それから、既存のSensorTagサンプルアプリをJust Works Pairing対応しようとしましたがうまくいかず、SamplePeripheralサンプルアプリをベースに、SensorTagサンプルアプリの機能を移植して、LINE用にカスタマイズしました。

カスタムファームウェアの使い方

  • サービス探索用serviceUUIDの設定
    Characteristic UUID:linethings.cのlinethingsProfileServiceUuidCharUUIDで示すUUID
    に、サービス探索用serviceUUID(16バイト)をリトルエンディアンでWriteします。

  • ハードウェアIDの設定
    Characteristic UUID:linethings.cのlinethingsProfileBeaconHwidCharUUIDで示すUUID
    に、ハードウェアID(5バイト)をWriteします。

  • LINE ThingsとLINE Beaconの切り替え
    Characteristic UUID:linethings.cのlinethingsProfileSwitchCharUUIDで示すUUID
    に、以下の値(1バイト)をWriteします。
    0x00:LINE Things対応デバイス
    0x01:LINE Beacon対応デバイス

LINE Things対応デバイスとしてLINEアプリから接続する

サービス探索用serviceUUIDを設定し、LINE Thingsに切り替わっていることを確認します。(アドバタイズデータを見ればわかります。)

LINEアプリを立ち上げて、右上の歯車のマークを選択して設定画面を表示します。
そこに、「LINE Things」という選択肢があるかと思います。それを選択します。

image.png

連携可能なデバイス に、SensorTag というのが現れていると思います。

image.png

それを選択します。以下のようなダイアログが表示されます。

image.png

「今すぐ利用」を押下すると、マイデバイスにSensorTagが追加されて、紐づけていたLIFFアプリが起動します。

image.png

後は、1回目の投稿と同じように、「接続」ボタンを押下して、チェックボックスをチェックすれば、センサー情報が取得されるかと思います。

image.png

ちなみに、1回目の投稿で、sensortag.jsにおいて、以下のようにしていましたが、今回取得したサービス探索用serviceUUIDに指定することで、BLEスキャン範囲を絞ることができます。

  • const UUID_SERVICE_LINETHINGS = "【LINE Thingsのサービス探索用serviceUUID】"; //1回目では使いません。
  • filters: [{services:[ /* service_uuid */ BluetoothUUID.canonicalUUID(0x180f) ]}], // 1回目ではとりあえずこんな感じ

LINE Beaconとして起動する

ハードウェアIDを設定し、LINE Beaconに切り替わっていることを確認します。(アドバタイズデータを見ればわかります。)
LINE Beaconから通知が飛ぶようになるので受ける側のRESTfulサーバを立ち上げます。

そのやり方は、過去の投稿の以下を参考にしてください。
 LINE Beaconを自宅に住まわせる

以上

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

Vue.jsでDraft.jsのハッシュタグプラグインに似た機能をなんとか作ってみる

背景

コンテンツ投稿型のアプリケーション開発の過程で、テキスト入力時や他の投稿を表示するときなど、FacebookやInstagramの投稿のように文章中にあるハッシュタグをハイライトさせる機能が必要な案件がありました。さらに、入力中のハッシュタグからインクリメントサーチでハッシュタグの予測語をリスト表示させ、予測語と入力中のハッシュタグを置換させるといった要望もあります。リッチテキストエディタを作る訳ではありませんが、その実装に向けてリサーチを行い、その備忘録を記します。

完成品はこんな感じです。

Desktop Mobile
desktop.gif mobile.gif

Facebookのハッシュタグハイライト

実際のFacebookでは投稿時にどのようにハイライトさせているか調べてみると、spanタグにcontentstateという属性が付いていて、どうやらその中身がハッシュタグに装飾を行なっている模様でした。

スクリーンショット 2019-03-24 1.11.35.png

Facebook投稿時のハッシュタグ部分のhtml
<span 
  contentstate="c { 
        &quot;entityMap": [object Object],
        "blockMap": OrderedMap { 
            "c9rd9": c { 
                "key": "c9rd9", 
                "type": "unstyled", 
                "text": "#aa", 
                "characterList": 
                List [ b { 
                        "style": OrderedSet {}, 
                        "entity": null }, 
                        b { 
                            "style": OrderedSet {}, 
                            "entity": null }, 
                            b { 
                                "style": OrderedSet {}, 
                                "entity": null } 
                    ], 
                "depth": 0, 
                "data": Map {} 
            } 
        }, 
        "selectionBefore": b { 
                                "anchorKey": "c9rd9", 
                                "anchorOffset": 0, 
                                "focusKey": "c9rd9", 
                                "focusOffset": 0, 
                                "isBackward": false, 
                                "hasFocus": true 
                            },
        "selectionAfter": b { 
                                "anchorKey": "c9rd9",
                                "anchorOffset": 3, 
                                "focusKey": "c9rd9", 
                                "focusOffset": 3, 
                                "isBackward": false, 
                                "hasFocus": true 
                            }}" 
  decoratedtext="#aa" 
  start="0" 
  end="3" 
  blockkey="c9rd9" 
  offsetkey="c9rd9-0-0" 
  data-offset-key="c9rd9-0-0" 
  class="_5zk7" 
  spellcheck="false">
  <span data-offset-key="c9rd9-0-0">
    <span data-text="true">#aa</span>
  </span>
</span>

中身は全く分からなかったですが、spanタグにあるcontentstateという見慣れない属性があり、これがヒントだと考えて調べてみるとDraft.jsというライブラリの機能のようでした。残念なことに、このDraft.jsはReact向けのライブラリのようで、今回はVueを使ったプロジェクトなので導入できません。そこで、似たような機能を搭載したVueコンポーネントを作ることにしました。

contenteditable

こちらの記事に、Draft.jsのハッシュタグハイライトの構造がまとめられていました。作りとしては入力されたテキストに対して、ハッシュタグを抽出する正規表現を用意し、検出したハッシュタグをcallbackで返し、そのハッシュタグを装飾用の別のReactのコンポーネントで置き換える?といった流れだと思います。ここで重要なのがcontenteditableというブラウザに実装されているhtmlの属性です。最初はtextareaを使った実装にしていましたが、文字数が増えていってコンテナサイズに収まりきらなくなった時に、textareaをリサイズするための処理が複雑になったので断念しました。このcontenteditable属性はDOMを直接編集可能にできるため、コンテナの高さを100%に設定しておけば、自動でリサイズしてくれます。ただし、v-modelが使えないため、watchなどでテキストのインプットをリアルタイムでトラッキングする方法が思いつきませんでした。私はDOMの変更を監視するMutationObserverを使ってテキストの入力を監視させました。

// targetはcontenteditable属性を持つ
const target = document.getElementById('input-true-text');
const observer = new MutationObserver(this.onObserveElement);    
const config = { 
                childList: true, 
                characterData: true,
                characterDataOldValue: true,
                subtree: true
              };

observer.observe(target, config);

innerHTMLで置換

contenteditableのコンテナの下レイヤーにもう一つコンテナを置き、contenteditableのコンテナで入力された内容をinnerHTMLで全て置き換えます。その際、ハッシュタグの内容だけタグで囲むように変更し、ハッシュタグを装飾できるようにしました。

structure.png

ここで注意しなければいけなかった点は、innerHTMLで置換される文字列に<&などの文字があるとhtmlとして認識されてしまうことです。そのため、一度全ての文字列からエスケープ文字だけ最初に置換してから、ハッシュタグの検出 => タグ文字とともに置換 を行います。

また、なぜかSafariブラウザとその他のブラウザでは複数行の改行をした時の改行コードの数が違っていたため、Safariブラウザ以外では改行文字を1つ削除させています。

methods
    onObserveElement(mutations) {
      mutations.forEach((mutation) => {
        const type = mutation.type

        switch(type) {
      // 文字入力に変化があればここ
          case 'characterData':
            this.replaceContent()
            break;
      
      // 行に変化があればここ
          case 'childList':
            this.replaceContent()
            break;

          default:
            break;        
        }
      })
    },
    replaceContent() {
      const target = document.getElementById('input-true-text');

      // NOTE: エスケープ文字を処理する
      const content = this.escapeHtml(target.innerText)
      const contentHTML = target.textContent

      // NOTE: 改行コードを削除(Safariブラウザ以外)
      const spaceExp = /^\n\n/gm
      const content2 = content.replace(spaceExp, function(match) {
        return '\n'
      })

      // NOTE: 新しいテキストを作成
      const srcContent = this.isSafariBrowser ? content : content2
      const self = this

      // ハッシュタグ文字を置換する
      const replaceContent = srcContent.replace(this.regExp, function(match) {
        const idStr = ' id=' + self.getUniqueStr()
        const result = '<i ' + self.hashtagStyle + idStr + '>' + match + '</i>'
        return result
      })

      // NOTE: 表示レイヤーに置換文字を適用
      const insertNode = document.getElementById('input-overlay')
      insertNode.innerHTML = replaceContent
    },

ハッシュタグの選択に対応

プレビュー時にハッシュタグを選択してハッシュタグ関連のコンテンツを表示させる、といった要望もありました。編集時にハッシュタグが選択できるようにさせると、ハッシュタグの選択なのか編集のための選択か判別できないため、編集モードとプレビューモードを分けました。プレビューモードでは単純に表示レイヤーをcontenteditableレイヤーの上に置き、DOMの編集をできないようにさせるだけです。その上で、表示レイヤーの<i>タグの変更を監視します。

  mounted() {    
    const overlayElm = document.getElementById('input-overlay')
    overlayElm.addEventListener("click", this.onSelectHashtag, false);
  },
  methods: {
   onSelectHashtag(e) {
      const target = e.target
      const tagName = target.tagName

      if (tagName === 'I') {
        const content = target.textContent
        this.$emit('onSelectHashtag', target)
      }
    },
  }

その他の機能

編集中のハッシュタグの置換も実装しました。入力中のハッシュタグからインクリメントサーチをして、予測語のハッシュタグを置換させるためです。

hashtag.gif

コンポーネントをライブラリ化

Vueのコンポーネントをvue-hashtag-textareaとしてライブラリ化しました。以下、変更できる装飾オプションです。

Options Type Description Default
textColor String ordinary text color black
font String wave height 14px "Noto Sans Japanese", sans-serif
hashtagBackgroundColor String background color under hashtag transparent
hashtagColor String hashtag color #ff0000
placeholder String placeholder on empty Sentence for placeholder #place #holder
isEditMode Boolean true: enable to edit but cannot select hashtag
false: enable to select hashtag but cannot edit
true

npmGitHubにあります。
なお、このライブラリはカーソルの管理は行なっておりません。そのため、編集中のハッシュタグを置換した後のカーソル位置は文末に置かれるので、少し使い勝手が悪いかもしれません。

Conclusion

今回の実装に当たって、ScrapBoxというサービスのハッシュタグ機能も参考にしました。入力された文字に対して1文字ずつspanタグを追加するといった実装になっています。ScrapBoxでもcontenteditableを使っていそうな感じで、ここを起点としてcontenteditableの使い方などを調べていっています。contenteditableはなかなかクセがある属性のようで、今回作成したvue-hashtag-texareaもまだ不十分な点が潜んでいると思います。その点をこれから発見して改善していければと思います。

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

Vue.jsでFacebookのハッシュタグハイライトに似た機能をなんとか作ってみる

背景

コンテンツ投稿型のアプリケーション開発の過程で、テキスト入力時や他の投稿を表示するときなど、FacebookやInstagramの投稿のように文章中にあるハッシュタグをハイライトさせる機能が必要な案件がありました。さらに、入力中のハッシュタグからインクリメントサーチでハッシュタグの予測語をリスト表示させ、予測語と入力中のハッシュタグを置換させるといった要望もあります。リッチテキストエディタを作る訳ではありませんが、その実装に向けてリサーチを行い、その備忘録を記します。

完成品はこんな感じです。

Desktop Mobile
desktop.gif mobile.gif

Facebookのハッシュタグハイライト

実際のFacebookでは投稿時にどのようにハイライトさせているか調べてみると、spanタグにcontentstateという属性が付いていて、どうやらその中身がハッシュタグに装飾を行なっている模様でした。

スクリーンショット 2019-03-24 1.11.35.png

Facebook投稿時のハッシュタグ部分のhtml
<span 
  contentstate="c { 
        &quot;entityMap": [object Object],
        "blockMap": OrderedMap { 
            "c9rd9": c { 
                "key": "c9rd9", 
                "type": "unstyled", 
                "text": "#aa", 
                "characterList": 
                List [ b { 
                        "style": OrderedSet {}, 
                        "entity": null }, 
                        b { 
                            "style": OrderedSet {}, 
                            "entity": null }, 
                            b { 
                                "style": OrderedSet {}, 
                                "entity": null } 
                    ], 
                "depth": 0, 
                "data": Map {} 
            } 
        }, 
        "selectionBefore": b { 
                                "anchorKey": "c9rd9", 
                                "anchorOffset": 0, 
                                "focusKey": "c9rd9", 
                                "focusOffset": 0, 
                                "isBackward": false, 
                                "hasFocus": true 
                            },
        "selectionAfter": b { 
                                "anchorKey": "c9rd9",
                                "anchorOffset": 3, 
                                "focusKey": "c9rd9", 
                                "focusOffset": 3, 
                                "isBackward": false, 
                                "hasFocus": true 
                            }}" 
  decoratedtext="#aa" 
  start="0" 
  end="3" 
  blockkey="c9rd9" 
  offsetkey="c9rd9-0-0" 
  data-offset-key="c9rd9-0-0" 
  class="_5zk7" 
  spellcheck="false">
  <span data-offset-key="c9rd9-0-0">
    <span data-text="true">#aa</span>
  </span>
</span>

中身は全く分からなかったですが、spanタグにあるcontentstateという見慣れない属性があり、これがヒントだと考えて調べてみるとDraft.jsというライブラリの機能のようでした。残念なことに、このDraft.jsはReact向けのライブラリのようで、今回はVueを使ったプロジェクトなので導入できません。そこで、似たような機能を搭載したVueコンポーネントを作ることにしました。

contenteditable

こちらの記事に、Draft.jsのハッシュタグハイライトの構造がまとめられていました。作りとしては入力されたテキストに対して、ハッシュタグを抽出する正規表現を用意し、検出したハッシュタグをcallbackで返し、そのハッシュタグを装飾用の別のReactのコンポーネントで置き換える?といった流れだと思います。ここで重要なのがcontenteditableというブラウザに実装されているhtmlの属性です。最初はtextareaを使った実装にしていましたが、文字数が増えていってコンテナサイズに収まりきらなくなった時に、textareaをリサイズするための処理が複雑になったので断念しました。このcontenteditable属性はDOMを直接編集可能にできるため、コンテナの高さを100%に設定しておけば、自動でリサイズしてくれます。ただし、v-modelが使えないため、watchなどでテキストのインプットをリアルタイムでトラッキングする方法が思いつきませんでした。私はDOMの変更を監視するMutationObserverを使ってテキストの入力を監視させました。

// targetはcontenteditable属性を持つ
const target = document.getElementById('input-true-text');
const observer = new MutationObserver(this.onObserveElement);    
const config = { 
                childList: true, 
                characterData: true,
                characterDataOldValue: true,
                subtree: true
              };

observer.observe(target, config);

innerHTMLで置換

contenteditableのコンテナの下レイヤーにもう一つコンテナを置き、contenteditableのコンテナで入力された内容をinnerHTMLで全て置き換えます。その際、ハッシュタグの内容だけタグで囲むように変更し、ハッシュタグを装飾できるようにしました。

structure.png

ここで注意しなければいけなかった点は、innerHTMLで置換される文字列に<&などの文字があるとhtmlとして認識されてしまうことです。そのため、一度全ての文字列からエスケープ文字だけ最初に置換してから、ハッシュタグの検出 => タグ文字とともに置換 を行います。

また、なぜかSafariブラウザとその他のブラウザでは複数行の改行をした時の改行コードの数が違っていたため、Safariブラウザ以外では改行文字を1つ削除させています。

methods
    onObserveElement(mutations) {
      mutations.forEach((mutation) => {
        const type = mutation.type

        switch(type) {
      // 文字入力に変化があればここ
          case 'characterData':
            this.replaceContent()
            break;
      
      // 行に変化があればここ
          case 'childList':
            this.replaceContent()
            break;

          default:
            break;        
        }
      })
    },
    replaceContent() {
      const target = document.getElementById('input-true-text');

      // NOTE: エスケープ文字を処理する
      const content = this.escapeHtml(target.innerText)
      const contentHTML = target.textContent

      // NOTE: 改行コードを削除(Safariブラウザ以外)
      const spaceExp = /^\n\n/gm
      const content2 = content.replace(spaceExp, function(match) {
        return '\n'
      })

      // NOTE: 新しいテキストを作成
      const srcContent = this.isSafariBrowser ? content : content2
      const self = this

      // ハッシュタグ文字を置換する
      const replaceContent = srcContent.replace(this.regExp, function(match) {
        const idStr = ' id=' + self.getUniqueStr()
        const result = '<i ' + self.hashtagStyle + idStr + '>' + match + '</i>'
        return result
      })

      // NOTE: 表示レイヤーに置換文字を適用
      const insertNode = document.getElementById('input-overlay')
      insertNode.innerHTML = replaceContent
    },

ハッシュタグの選択に対応

プレビュー時にハッシュタグを選択してハッシュタグ関連のコンテンツを表示させる、といった要望もありました。編集時にハッシュタグが選択できるようにさせると、ハッシュタグの選択なのか編集のための選択か判別できないため、編集モードとプレビューモードを分けました。プレビューモードでは単純に表示レイヤーをcontenteditableレイヤーの上に置き、DOMの編集をできないようにさせるだけです。その上で、表示レイヤーの<i>タグの変更を監視します。

  mounted() {    
    const overlayElm = document.getElementById('input-overlay')
    overlayElm.addEventListener("click", this.onSelectHashtag, false);
  },
  methods: {
   onSelectHashtag(e) {
      const target = e.target
      const tagName = target.tagName

      if (tagName === 'I') {
        const content = target.textContent
        this.$emit('onSelectHashtag', target)
      }
    },
  }

その他の機能

編集中のハッシュタグの置換も実装しました。入力中のハッシュタグからインクリメントサーチをして、予測語のハッシュタグを置換させるためです。

hashtag.gif

コンポーネントをライブラリ化

Vueのコンポーネントをvue-hashtag-textareaとしてライブラリ化しました。以下、変更できる装飾オプションです。

Options Type Description Default
textColor String ordinary text color black
font String wave height 14px "Noto Sans Japanese", sans-serif
hashtagBackgroundColor String background color under hashtag transparent
hashtagColor String hashtag color #ff0000
placeholder String placeholder on empty Sentence for placeholder #place #holder
isEditMode Boolean true: enable to edit but cannot select hashtag
false: enable to select hashtag but cannot edit
true

npmGitHubにあります。
なお、このライブラリはカーソルの管理は行なっておりません。そのため、編集中のハッシュタグを置換した後のカーソル位置は文末に置かれるので、少し使い勝手が悪いかもしれません。

Conclusion

今回の実装に当たって、ScrapBoxというサービスのハッシュタグ機能も参考にしました。入力された文字に対して1文字ずつspanタグを追加するといった実装になっています。ScrapBoxでもcontenteditableを使っていそうな感じで、ここを起点としてcontenteditableの使い方などを調べていっています。contenteditableはなかなかクセがある属性のようで、今回作成したvue-hashtag-texareaもまだ不十分な点が潜んでいると思います。その点をこれから発見して改善していければと思います。

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

多言語切り替えをjavascriptでサクッっと

GOAL

XボタンをクリックするとYクラスをもつ要素を消しつつ、Zクラスを持つ要素を表示する。
みたいな処理を利用して、URLを変更せずに多言語対応

How to

まず、用意するのは2つのボタンと2つのクラス。
⑴onclick="changeJa();"がついた要素と
⑵onclick="changeen();"がついた要素。
⑶ja-conという日本語のコンテンツ(ja-conクラス)
⑷en-conという英語のコンテンツ(en-conクラス)

test.html
<div>
    <p onclick="changeJa();">Japan</p>
    <p onclick="changeEn();">English</p>
</div>

<p class="en-con">
    Hello world
</p>
<p class="ja-con">
    こんにちわ!
</p>
test.css
.en-con {
    display: none;
}
test.js
<script type="text/javascript">
    function changeEn(){
        var ja = document.getElementsByClassName('ja-con');
        var en = document.getElementsByClassName('en-con');
        for(i=0;i<ja.length;i++){
            ja[i].style.display = "none";
            en[i].style.display = "block";
        }
    }
</script>

デフォルトはja-con(日本語コンテンツ)なので、cssでen-conを非表示にしています。
ボタンがクリックされるたびに、片方のdisplayをnoneにし、表示したい方をdisplay:block;にしています。

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

Webpack4 - How to Get Build Mode Parameters?

1. Install

yarn add -D webpack-mode

2. Modified package.json scripts part.

"scripts": {
-    "dev": "webpack --config webpack.dev.js",
-    "prod": "webpack --config webpack.prod.js,
+    "dev": "webpack --mode=development --config webpack.dev.js",
+    "prod": "webpack --mode=production --config webpack.prod.js,
},

3. Used Example.

webpack.common.js
...
const { isProduction } = require('webpack-mode');
...
module: {
    rules: [
      ...
      {
        test: /\.(ico|plist|png|jpg|gif|mp3)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[hash].[ext]',
              outputPath: 'assets',
              publicPath: isProduction ? 'assets' : 'publish/assets', <--- Add code here
            },
          },
        ],
      },
    ],
  },

https://github.com/mckomo/webpack-mode

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

Webpack4 - How to Get Build Mode Parameter?

Get build mode parameter is a hard way in Webpack4. I found the issues here. Anyway, I found a solution.

1. Install

yarn add -D webpack-mode

2. Modified package.json scripts part.

"scripts": {
-    "dev": "webpack --config webpack.dev.js",
-    "prod": "webpack --config webpack.prod.js,
+    "dev": "webpack --mode=development --config webpack.dev.js",
+    "prod": "webpack --mode=production --config webpack.prod.js,
},

3. Used Example.

webpack.common.js
...
const { isProduction } = require('webpack-mode');
...
module: {
    rules: [
      ...
      {
        test: /\.(ico|plist|png|jpg|gif|mp3)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[hash].[ext]',
              outputPath: 'assets',
              publicPath: isProduction ? 'assets' : 'publish/assets', <--- Add code here
            },
          },
        ],
      },
    ],
  },

https://github.com/mckomo/webpack-mode

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

lighthouseのスコアリング詳細

概要

lighthouseのperformace項目の評価指標とどのようにスコアを算出しているかについてのまとめです。

評価指標

47a13957-272d-415c-9364-93b5ffad7365-1920x502r.png

lighthouseのperformanceでは上記6つの指標が利用されています。
最終スコアに影響するのは、Estimated Input Latencyを抜いた5つです。
以下、それぞれの指標について解説していきます。

First Contentful Paint (コンテンツの初回ペイント)

fe2e044d-98d0-4ee3-97a2-a8c520198443-1920x684r.png

First Contentful Paint (FCP) はブラウザがテキストや画像など何かしらのコンテンツをレンダリングし始めた時間です。
FCPのタイミングが遅い場合、

  • サーバーのレスポンスが遅い
  • クリティカルレンダリングパスが最適化されていない

などの原因が考えられます。

First Meaningful Paint (意味のあるコンテンツの初回ペイント)

First Meaningful Paintはユーザーにとって何かしら意味のある (役に立つ) コンテンツが描画された時間を示します。
何を持って「意味のある」とみなすかは、Webページのコンテンツや性質によって異なるため標準化が難しく明確な仕様は策定されていません。
一般的にはそのWebページ内の最も重要なコンテンツが描画された時間に当たります。
346f5a14-480b-4433-a320-a346e85c9796-1920x839r.png

レイアウトの変更を元にFMPの近似値を算出する評価アプローチについては、以下にまとめられてます。

Speed Index (速度インデックス)

Speed Indexは他の指標とは少し変わった指標になります。
他の指標は基本的にあるタイミング(マイルストーン)に到達するまでの時間を評価するのに対して、Speed Indexはページロードが完了するまでのプロセスを評価するための指標です。
1d575c18-c410-4057-a15b-42ec5d42f810-1920x506r.png

この画像は2つのページ(上がA、下がB)のロードが完了するまでのキャプチャですが、AとBはロード完了までの時間は同じですが、Aは早い段階 (1.0s) でFirstViewの大半が描画されているのに対して、Bは後半 (11.0s) に描画されています。
この場合、AとBを比較するとAの方がパフォーマンスが優れている(Speed Indexは低い)ということになります。

このページロードの進捗をX軸を経過時間、Y軸を描画割合としてグラフで表すと、以下のようになります。
91de57c1-9b6c-4634-b1d7-81c105511fcb-1920x1408r.png

また、AとBの未表示領域を比較すると以下のようになります。
cbc3b080-9b1a-439e-b7c0-776795f54fa2-1920x725r.png

図を見ても分かる通り、SpeedIndexは (単位時間×未描画領域割合) の総和になります。
そのため、SpeedIndexの値が低いほど優れているということになります。
注意点としてはSpeedIndexはページ全体の描画量ではなく、Viewport内の範囲(FirstView)の表示割合をみています。

このときどうやって、「描画されている割合」を算出するかについてですが、主に以下の2つの方法があります。

  1. 単位時間ごとにキャプチャを残しておき、最終的に表示された画面と経過時間時点での画面との差分からどのくらいの割合が描画されているかを判定
  2. ブラウザのLayoutイベント等から判定

パフォーマンス測定サービスのwebpagetestでは1、lighthouseではspeedlineというツールを使って2の方法で計測しています。

最終画面との比較によっての算出になるため、FirstView内でサイズの大きい画像を読み込んだり、カルーセルなど後からjavascriptを用いて動的にコンテンツを表示したりすることは、SpeedIndexにおいては不利に働きやすいと言われています。

First CPU Idle (CPU の初回アイドル)

First CPU Idle (FCI) は、ブラウザのメインスレッドがアイドル状態 (待ち状態) になり、ユーザーの入力を受け付けられると想定されるタイミングを示します。

算出のアプローチについては、First Meaningful Paint以降、最初にブラウザのメインスレッドでLong Task(50ms以上のタスク)が5秒間発生していないタイミングを求めるといった方法がありますが、First Meaningful Paint自体が標準化されていない指標のため、FCIもまた標準化されているわけではありません。

Time to Interactive (インタラクティブになるまでの時間)

Time to Interactive (TTI) は、FCIと似てますが、Web ページのロードが終わりユーザーの入力に対してすばやく応答できると予想されるタイミングを指します。

算出は、First Meaningful Paint以降、最初にネットワークリクエストが2回以内に収まり、ブラウザのメインスレッドでLong Task(50ms以上のタスク)が5秒間発生していないタイミングを求めるといった方法がありますが、FCIと同じく標準化されているわけではありません。
30b1c485-377e-40cf-82db-4ac579720841-1920x1124r.png

算出アプローチの詳細については以下にまとめられています。

分析や広告タグを読み込むとなぜパフォーマンスが悪くなるのかという疑問が上がることがありますが、一般的にはそういったサードパーティーのスクリプトは、1つ読み込むだけでもその先でリダイレクトし複数のリクエストを発生させ、ブラウザのメインスレッド内でスクリプトの解析や処理を行うため、上記のFCIやTTIのタイミングを遅らせます。(同期的に読み込むスクリプトは、コンテンツの表示自体も遅らせるので更に影響ありますが、、)

同様に、Twitter, Facebook, YoutubeなどのSocial Widgetなどもperformanceを劣化させる要因になり得るので導入の際には注意が必要です。AMPでスクリプトを制限しているのもこういった理由からです。

もちろんそういったタグを全く読み込まないということは厳しいので、極力無駄なタグは読み込まない努力が必要です。

Estimated Input Latency (入力の推定待ち時間)

Estimated Input Latencyは、ユーザーの入力(操作)に応答するまでの推定時間です。
この指標は直接最終スコアには影響しませんが、ユーザーに快適な操作を提供するために必要な指標です。

この指標の元となる思想には、RAIL パフォーマンスモデル というものがあり、それによるとユーザーの入力に対して100ms以内に応答することが理想とされています。

評価指標の重み付け

lighthouseは上記の指標すべてを均等にトータルスコアに反映しているわけではなくそれぞれの指標に重みを付けてスコアを計算しています。
それぞれの指標の重みに関しては以下の通りです。

  • First Contentful Paint => 3
  • First Meaningful Paint => 1
  • Speed Index => 4
  • First CPU Idle => 2
  • Time to Interactive => 5
  • Estimated Input Latency => 0

この指標の重み付けについてはGoogleが資料を公開しています。

同じシート内に ScoringCalculator を用意してくれており、これによってどの指標がどの程度短縮されたらどのくらいのスコアになるかの試算や、スコア90になるためにはどの指標をどの程度縮める必要があるかの逆算などが行えます。

[補足] 上記はlighthouse v3の重み付けですが軽く試してみたところv4でも重み付け自体は変わっていなそうでした。


参考

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

TI SensorTagをブラウザから使う(1):with Chromeブラウザ

TIが販売している小型で電池駆動のSensorTagを使ってみます。
 http://www.tij.co.jp/tool/jp/CC2650STK

計10個のセンサが付いているにもかかわらず、低価格です。

  • デジタル・マイク
  • 磁気センサ
  • 湿度
  • 圧力
  • 加速度計
  • ジャイロスコープ
  • 磁力計
  • 物体の温度
  • 周囲温度

しかも、デバッガボードCC-DEVPACK-DEBUGを使うと、ファームウェアのカスタマイズができます。これも低価格です。
 https://service.macnica.co.jp/library/120701

いろいろできそうなので、以降、2回の投稿に渡っていじってみます。

1回目は、WebBluetooth APIを使ってブラウザから接続し、センサ情報を表示します。WebBluetooth APIが対応しているブラウザであれば大丈夫なのですが、今回は定番のChromeを使います。
Android、Windows10で動くことを確認しました。(Windows10のChromeで動いたのは驚きでした)

2回目は、LINE ThingsのBLE機能を使って接続します。単に、LINE Thingsの勉強のためです。
ですが、2回目は、デバッガボードCC-DEVPACK-DEBUGを所有している方のみが対象です。LINE Thingsに対応させるためにファームウェアのカスタマイズが必要だからです。

WebBluetooth APIを使ったSensorTag用クラスを作成する

単に、HTMLとJavascriptを書いて、HTTPSからブラウズできる場所に置くだけです。
WebBluetooth APIを使うので、HTTPSである必要があります。

まずは、SensorTagを扱いやすくするためのクラスを作成します。
WebBluetooth APIを存分に使っています。

以下を参考にさせていただきました。

sensortag.js
'use stricts';

const UUID_SERVICE_LINETHINGS = "【LINE Thingsのサービス探索用serviceUUID】"; //1回目では使いません。 

const UUID_SERVICE_IR_TEMPERATURE = 'f000aa00-0451-4000-b000-000000000000';
const UUID_CHAR_IR_TEMPERATURE_DATA = 'f000aa01-0451-4000-b000-000000000000';
const UUID_CHAR_IR_TEMPERATURE_ENABLE = 'f000aa02-0451-4000-b000-000000000000';
const UUID_CHAR_IR_TEMPERATURE_PERIOD = 'f000aa03-0451-4000-b000-000000000000';

const UUID_SERVICE_HUMIDITY = 'f000aa20-0451-4000-b000-000000000000';
const UUID_CHAR_HUMIDITY_DATA = 'f000aa21-0451-4000-b000-000000000000';
const UUID_CHAR_HUMIDITY_ENABLE = 'f000aa22-0451-4000-b000-000000000000';
const UUID_CHAR_HUMIDITY_PERIOD = 'f000aa23-0451-4000-b000-000000000000';

const UUID_SERVICE_BAROMETRIC_PRESSURE = 'f000aa40-0451-4000-b000-000000000000';
const UUID_CHAR_BAROMETRIC_PRESSURE_DATA = 'f000aa41-0451-4000-b000-000000000000';
const UUID_CHAR_BAROMETRIC_PRESSURE_ENABLE = 'f000aa42-0451-4000-b000-000000000000';
const UUID_CHAR_BAROMETRIC_PRESSURE_PERIOD = 'f000aa44-0451-4000-b000-000000000000';

const UUID_SERVICE_MOVEMENT = 'f000aa80-0451-4000-b000-000000000000';
const UUID_CHAR_MOVEMENT_DATA = 'f000aa81-0451-4000-b000-000000000000';
const UUID_CHAR_MOVEMENT_ENABLE = 'f000aa82-0451-4000-b000-000000000000';
const UUID_CHAR_MOVEMENT_PERIOD = 'f000aa83-0451-4000-b000-000000000000';

const UUID_SERVICE_OPTICAL = 'f000aa70-0451-4000-b000-000000000000';
const UUID_CHAR_OPTICAL_DATA = 'f000aa71-0451-4000-b000-000000000000';
const UUID_CHAR_OPTICAL_ENABLE = 'f000aa72-0451-4000-b000-000000000000';
const UUID_CHAR_OPTICAL_PERIOD = 'f000aa73-0451-4000-b000-000000000000';

const UUID_SERVICE_IO = 'f000aa64-0451-4000-b000-000000000000';
const UUID_CHAR_IO_DATA = 'f000aa65-0451-4000-b000-000000000000';
const UUID_CHAR_IO_CONFIG = 'f000aa66-0451-4000-b000-000000000000';

const UUID_SERVICE_SIMPLE_KEYS = BluetoothUUID.canonicalUUID(0xffe0);
const UUID_CHAR_SIMPLE_KEYS_DATA = BluetoothUUID.canonicalUUID(0xffe1);

class SensorTag{
    constructor(){
        this.bluetoothDevice = null;
        this.characteristics = new Map();
    }

    is_opened(){
        return this.bluetoothDevice ? true : false;
    }

    open(){
        return this.requestDevice(UUID_SERVICE_LINETHINGS);
    }

    close() {
        if (!this.is_opened())
            throw "Bluetooth Device is not opened";

        return Promise.resolve()
        .then(() =>{
            if (this.bluetoothDevice.gatt.connected) {
                console.log('Execute : disconnect');
                this.bluetoothDevice.gatt.disconnect();
                this.bluetoothDevice = null;
                this.characteristics.clear();
            } else {
                this.bluetoothDevice = null;
                this.characteristics.clear();
                throw "Bluetooth Device is already disconnected";
            }
        });
    }

    setup(){
        console.log('Execute : setup');

        return this.bluetoothDevice.gatt.connect()
        .then(server => {
            return this.setup_ir_tempeature(server);
        })
        .then(server => {
            return this.setup_humidity(server);
        })
        .then(server => {
            return this.setup_barometric_pressure(server);
        })
        .then(server => {
            return this.setup_movement(server);
        })
        .then(server => {
            return this.setup_optical(server);
        })
        .then(server => {
            return this.setup_io(server);
        })
        .then(server => {
            return this.setup_simple_keys(server);
        })
        .then(server =>{
            console.log('setup done');
            return this.bluetoothDevice.name;
        });
    }

    set_enable_movement(xg, yg, zg, xa, ya, za, mag, wakeon, arange){
        if (!this.is_opened())
            return;

        var bits = 0;
        bits |= (xg ? 0x01 : 0x00) << 13;
        bits |= (yg ? 0x01 : 0x00) << 14;
        bits |= (zg ? 0x01 : 0x00) << 15;
        bits |= (xa ? 0x01 : 0x00) << 10;
        bits |= (ya ? 0x01 : 0x00) << 11;
        bits |= (za ? 0x01 : 0x00) << 12;
        bits |= (mag ? 0x01 : 0x00) << 9;
        bits |= (wakeon ? 0x01 : 0x00) << 8;
        bits |= (arange & 0x03) << 6;
        this.paRange = Math.pow(2, (arange & 0x03) + 1);
        return this.characteristics.get(UUID_CHAR_MOVEMENT_ENABLE).writeValue(Uint8Array.from([(bits >> 8) & 0xff, bits & 0xff]));
    }

    set_enable(uuid, enable){
        if (!this.is_opened())
            return;

        return this.characteristics.get(uuid).writeValue(Uint8Array.from([enable ? 0x01 : 0x00]));
    }

    set_period(uuid, period){
        return this.characteristics.get(uuid).writeValue(Uint8Array.from([period & 0xff]));
    }

    set_callback(callback){
        this.callback = callback;
    }

    onDataChanged(event){
        console.log('onDataChanged');
        let characteristic = event.target;

        switch(characteristic.uuid){
            case UUID_CHAR_IR_TEMPERATURE_DATA:{
                var ambientTemperature = characteristic.value.getUint16(2, true) / 128.0;
                var objectTemperature = characteristic.value.getUint16(0, true) / 128.0;
                if( this.callback ){
                    this.callback({
                        type: 'temperature',
                        ambient_temperature: ambientTemperature,
                        object_temperature: objectTemperature
                    });
                }
                break;
            }
            case UUID_CHAR_HUMIDITY_DATA:{
                var temperature  = -40 + ((165  * characteristic.value.getUint16(0, true)) / 65536.0);
                var humidity  = characteristic.value.getUint16(2, true) * 100 / 65536.0;
                if( this.callback ){
                    this.callback({
                        type: 'humidity',
                        temperature: temperature,
                        humidity : humidity 
                    });
                }
                break;
            }
            case UUID_CHAR_BAROMETRIC_PRESSURE_DATA:{
                var flTempBMP;
                var flPressure;

                flTempBMP = (characteristic.value.getUint16(0, true) | (characteristic.value.getUint8(2) << 16))/ 100.0;
                flPressure = (characteristic.value.getUint16(3, true) | (characteristic.value.getUint8(5) << 16)) / 100.0;
                if( this.callback ){
                    this.callback({
                        type: 'pressure',
                        pressure: flPressure,
                        temperature: flTempBMP
                    });
                }

                break;
            }
            case UUID_CHAR_MOVEMENT_DATA:{
                // 250 deg/s range
                var xG = characteristic.value.getInt16(0, true) / 128.0;
                var yG = characteristic.value.getInt16(2, true) / 128.0;
                var zG = characteristic.value.getInt16(4, true) / 128.0;

                // we specify 8G range in setup
                var xA = characteristic.value.getInt16(6, true) / (32768.0 / this.paRange);
                var yA = characteristic.value.getInt16(8, true) / (32768.0 / this.paRange);
                var zA = characteristic.value.getInt16(10, true) / (32768.0 / this.paRange);

                // magnetometer (page 50 of http://www.invensense.com/mems/gyro/documents/RM-MPU-9250A-00.pdf)
                var xM = characteristic.value.getInt16(12, true);
                var yM = characteristic.value.getInt16(14, true);
                var zM = characteristic.value.getInt16(16, true);
                if( this.callback ){
                    this.callback({
                        type: 'movement',
                        xG: xG,
                        yG: yG,
                        zG: zG,
                        xA: xA,
                        yA: yA,
                        zA: zA,
                        xM: xM,
                        yM: yM,
                        zM: zM,
                    });
                }
                break;
            }
            case UUID_CHAR_OPTICAL_DATA:{
                var rawLux = characteristic.value.getUint16(0, true);

                var exponent = (rawLux & 0xF000) >> 12;
                var mantissa = (rawLux & 0x0FFF);

                var flLux = mantissa * Math.pow(2, exponent) / 100.0;
                if( this.callback ){
                    this.callback({
                        type: 'optical',
                        luminance : flLux
                    });
                }

                break;
            }
            case UUID_CHAR_SIMPLE_KEYS_DATA:{
                if( this.callback ){
                    this.callback({
                        type: 'keys',
                        keys : characteristic.value.getUint8(0)
                    });
                }

                break;

            }
            default:
                console.log('Unkown data', characteristic);
                break;
        }
    }

    onDisconnect(event){
        console.log('onDisconnect');
    }

    requestDevice(service_uuid){
        return new Promise((resolve, reject) =>{
            liff.init(data => resolve(data), error => reject(error) );
        })
        .then(data =>{
            console.log('Execute : requestDevice(liff)');
            return this.liffCheckAvailablityAndDo()
            .then( () =>{
                return liff.bluetooth.requestDevice();
            })
            .then(device =>{
                console.log("requestDevice OK");
                this.characteristics.clear();
                this.bluetoothDevice = device;
                this.bluetoothDevice.addEventListener('gattserverdisconnected', (event) => {
                    this.onDisconnect(event);
                });
                return this.bluetoothDevice.name;
            });
        })
        .catch(error =>{
            console.log('Execute : requestDevice(normal)');
            return navigator.bluetooth.requestDevice({
                filters: [{services:[ /* service_uuid */ BluetoothUUID.canonicalUUID(0x180f) ]}], // 1回目ではとりあえずこんな感じ
                  optionalServices: [
                      UUID_SERVICE_IR_TEMPERATURE,
                      UUID_SERVICE_HUMIDITY,
                      UUID_SERVICE_BAROMETRIC_PRESSURE,
                      UUID_SERVICE_MOVEMENT,
                      UUID_SERVICE_OPTICAL,
                      UUID_SERVICE_IO,
                      UUID_SERVICE_SIMPLE_KEYS
                ]
            })
            .then(device => {
                console.log("requestDevice OK");
                this.characteristics.clear();
                this.bluetoothDevice = device;
                this.bluetoothDevice.addEventListener('gattserverdisconnected', (event) => {
                    this.onDisconnect(event)
                });
                return this.bluetoothDevice.name;
            });
        });
    }

    async liffCheckAvailablityAndDo() {
        console.log('calling liffCheckAvailablityAndDo');

        await liff.initPlugins(['bluetooth']);
        for( var i = 0 ; i < 10 ; i++ ){
            var isAvailable = await liff.bluetooth.getAvailability();
            if( isAvailable )
                return;
            await wait_async(3000);
        }
        throw 'error liff.bluetooth.getAvailability';
    }

    setCharacteristic(service, characteristicUuid) {
        return service.getCharacteristic(characteristicUuid)
        .then(characteristic => {
            console.log('setCharacteristic : ' + characteristicUuid);
            this.characteristics.set(characteristicUuid, characteristic);
            return service;
        });
    }

    startNotify(uuid) {
        console.log('Execute : startNotifications');
        var characteristic = this.characteristics.get(uuid);
        if( characteristic === undefined )
            throw "Not Connected";

        characteristic.addEventListener('characteristicvaluechanged', (event) =>{
            this.onDataChanged(event);
        });
        return characteristic.startNotifications();
    }

    stopNotify(uuid){
        console.log('Execute : stopNotifications');
        var characteristic = this.characteristics.get(uuid);
        if( characteristic === undefined )
            throw "Not Connected";

        return characteristic.stopNotifications();
    }

    setup_ir_tempeature(server){
        return server.getPrimaryService(UUID_SERVICE_IR_TEMPERATURE)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_IR_TEMPERATURE_DATA),
                this.setCharacteristic(service, UUID_CHAR_IR_TEMPERATURE_ENABLE),
                this.setCharacteristic(service, UUID_CHAR_IR_TEMPERATURE_PERIOD)
            ]);
        })
        .then(values =>{
            return this.startNotify(UUID_CHAR_IR_TEMPERATURE_DATA);
        })
        .then(()=>{
            return server;
        });
    }

    setup_humidity(server){
        return server.getPrimaryService(UUID_SERVICE_HUMIDITY)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_HUMIDITY_DATA),
                this.setCharacteristic(service, UUID_CHAR_HUMIDITY_ENABLE),
                this.setCharacteristic(service, UUID_CHAR_HUMIDITY_PERIOD)
            ]);
        })
        .then(values =>{
            return this.startNotify(UUID_CHAR_HUMIDITY_DATA);
        })
        .then(()=>{
            return server;
        });
    }

    setup_barometric_pressure(server){
        return server.getPrimaryService(UUID_SERVICE_BAROMETRIC_PRESSURE)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_BAROMETRIC_PRESSURE_DATA),
                this.setCharacteristic(service, UUID_CHAR_BAROMETRIC_PRESSURE_ENABLE),
                this.setCharacteristic(service, UUID_CHAR_BAROMETRIC_PRESSURE_PERIOD)
            ]);
        })
        .then(values =>{
            return this.startNotify(UUID_CHAR_BAROMETRIC_PRESSURE_DATA);
        })
        .then(()=>{
            return server;
        });
    }

    setup_movement(server){
        return server.getPrimaryService(UUID_SERVICE_MOVEMENT)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_MOVEMENT_DATA),
                this.setCharacteristic(service, UUID_CHAR_MOVEMENT_ENABLE),
                this.setCharacteristic(service, UUID_CHAR_MOVEMENT_PERIOD)
            ]);
        })
        .then(values =>{
            return this.startNotify(UUID_CHAR_MOVEMENT_DATA);
        })
        .then(()=>{
            return server;
        });
    }

    setup_optical(server){
        return server.getPrimaryService(UUID_SERVICE_OPTICAL)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_OPTICAL_DATA),
                this.setCharacteristic(service, UUID_CHAR_OPTICAL_ENABLE),
                this.setCharacteristic(service, UUID_CHAR_OPTICAL_PERIOD)
            ]);
        })
        .then(values =>{
            return this.startNotify(UUID_CHAR_OPTICAL_DATA);
        })
        .then(()=>{
            return server;
        });
    }

    setup_io(server){
        return server.getPrimaryService(UUID_SERVICE_IO)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_IO_DATA),
                this.setCharacteristic(service, UUID_CHAR_IO_CONFIG)
            ]);
        })
        .then(()=>{
            return server;
        });
    }

    setup_simple_keys(server){
        return server.getPrimaryService(UUID_SERVICE_SIMPLE_KEYS)
        .then(service =>{
            return Promise.all([
                this.setCharacteristic(service, UUID_CHAR_SIMPLE_KEYS_DATA)
            ]);
        })
        .then(values =>{
            return this.startNotify(UUID_CHAR_SIMPLE_KEYS_DATA);
        })
        .then(()=>{
            return server;
        });
    }
}

function wait_async(timeout){
    return new Promise((resolve, reject) =>{
        setTimeout(resolve, timeout);
    });
}

UUID_SERVICE_LINETHINGS は1回目では使わないので、とりあえず変更する必要はありません。

クラスの実装を一部補足します。

  • open()

まず何よりこのメソッドを呼び出します。

 liff.init(data => resolve(data), error => reject(error) )

これは、LINE Thingsを使うためのLINEアプリ(LIFF)から呼ばれたかどうかを判別するためのコードです。1回目はChromeからの呼び出しなのでエラーとなり、catchの処理に移ります。

 navigator.bluetooth.requestDevice

引数に指定したサービスUUIDを持ったBLEデバイスを検索(Scan)します。素のSensorTagではバッテリーサービス(0x180f)を持っているので、それを指定しています。

  • setup()

センサー情報を読み出すためのCharacteristicを検索するとともに、センサー情報のNotifyを有効にします。
CharacteristicのUUIDは決まっているので、それを各Primary Serviceで指定しています。

  • set_enable()

センサー計測を有効にします。setup()ではNotifyができる状態になっただけです。センサー情報は取得の有効化・無効化はこのset_enable()で切り替えます。

  • set_period()

センタ情報のNotify間隔を指定します。デフォルトは1秒間隔になっています。

  • set_callback()

これは、センサー情報の取得はReadではなくNotifyで受信するようにしたいため、このメソッドに受信するコールバック関数を指定します。
SensorTagからNotifyされてくるデータを各センサーのタイプに合わせてコンバートします。
(※気圧のコンバートのための計算式がわかりませんでした。。。。MovementのEnableマスクも怪しい。。。)

あとは、このクラスを使ってブラウザに表示させます。Vueを使っています。

start.js
'use strict';

var sensortag = new SensorTag();

var vue_options = {
    el: "#top",
    data: {
        movement: {},
        temperature: {},
        humidity: {},
        pressure: {},
        optical: {},
        temperature_enable : false,
        movement_enable : false,
        humidity_enable : false,
        pressure_enable : false,
        optical_enable : false,
        button_title: '接続',
        left_key: null,
        right_key: null,

        progress_title: '',
    },
    computed: {
    },
    methods: {
        start: async function(){
            if( !sensortag.is_opened() ){
                try{
                    this.button_title = '接続中';
                    sensortag.set_callback(this.on_receive);
                    await sensortag.open();
                    await sensortag.setup();
                    this.button_title = '切断';
                }catch(error){
                    console.log(error);
                    alert(error);
                    this.button_title = '接続';
                }
            }else{
                try{
                    this.button_title = '切断中';
                    sensortag.close();
                    this.button_title = '接続';
                }catch(error){
                    console.log(error);
                    this.button_title = '切断';
                }
            }
        },
        set_enable: async function(type){
            if( type == 'movement'){
                if( !this.movement_enable )
                    await sensortag.set_enable_movement(true, true, true, true, true, true, true, false, 3);
                else
                    await sensortag.set_enable_movement(false, false, false, false, false, false, false, false, 0);
            }else{
                var uuid;
                var value;
                switch(type){
                    case 'temperature':
                        uuid = UUID_CHAR_IR_TEMPERATURE_ENABLE;
                        value = !this.temperature_enable;
                        break;
                    case 'humidity':
                        uuid = UUID_CHAR_HUMIDITY_ENABLE;
                        value = !this.humidity_enable;
                        break;
                    case 'pressure':
                        uuid = UUID_CHAR_BAROMETRIC_PRESSURE_ENABLE;
                        value = !this.pressure_enable;
                        break;
                    case 'optical':
                        uuid = UUID_CHAR_OPTICAL_ENABLE;
                        value = !this.optical_enable;
                        break;
                }
                await sensortag.set_enable(uuid, value);
            }
        },
        on_receive: function(data){
            console.log(data);
            switch( data.type ){
                case 'temperature':
                    this.temperature = data;
                    break;
                case 'humidity':
                    this.humidity = data;
                    break;
                case 'pressure':
                    this.pressure = data;
                    break;
                case 'movement':
                    this.movement = data;
                    break;
                case 'optical':
                    this.optical = data;
                    break;
                case 'keys':
                    if( data.keys & 0x01 )
                        this.left_key = true;
                    else
                        this.left_key = false;
                    if( data.keys & 0x02 )
                        this.right_key = true;
                    else
                        this.right_key = false;
                    break;
            }
        }
    },
    created: function(){
    },
    mounted: function(){
    }
};
var vue = new Vue( vue_options );

以下は、HTMLファイルです。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>SensortTag</title>

  <script src="https://d.line-scdn.net/liff/1.0/sdk.js"></script>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>SensorTag</h1>

        <button class="btn btn-default" v-on:click="start()">{{button_title}}</button><br>
        <br>
        <input type="checkbox" v-model="temperature_enable" v-on:click="set_enable('temperature')">
        IR Temperature Sensor<br>
        <label>ambient_temperature</label> {{temperature.ambient_temperature}}<br>
        <label>object_temperature</label> {{temperature.object_temperature}}<br>
        <br>
        <input type="checkbox" v-model="humidity_enable" v-on:click="set_enable('humidity')">
        Humidity Sensor<br>
        <label>temperature</label> {{humidity.temperature}}<br>
        <label>humidity</label> {{humidity.humidity}}<br>
        <br>
        <input type="checkbox" v-model="pressure_enable" v-on:click="set_enable('pressure')">
        Barometric Pressure Sensor<br>
        <label>pressure</label> {{pressure.pressure}}<br>
        <label>temperature</label> {{pressure.temperature}}<br>
        <br>
        <input type="checkbox" v-model="optical_enable" v-on:click="set_enable('optical')">
        Optical Sensor<br>
        <label>luminance</label> {{optical.luminance}}<br>
        <br>
        Keys<br>
        <label>left_key</label> {{left_key}}<br>
        <label>right_key</label> {{right_key}}<br>
        <br>
        <input type="checkbox" v-model="movement_enable" v-on:click="set_enable('movement')">
        Movement Sensor<br>
        <label>xG</label> {{movement.xG}}<br>
        <label>yG</label> {{movement.yG}}<br>
        <label>zG</label> {{movement.zG}}<br>
        <label>xA</label> {{movement.xA}}<br>
        <label>yA</label> {{movement.yA}}<br>
        <label>zA</label> {{movement.zA}}<br>
        <label>xM</label> {{movement.xM}}<br>
        <label>yM</label> {{movement.yM}}<br>
        <label>zM</label> {{movement.zM}}<br>

        <div class="modal fade" id="progress">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title">{{progress_title}}</h4>
                    </div>
                    <div class="modal-body">
                        <center><progress max="100" /></center>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script src="js/sensortag.js"></script>
    <script src="js/start.js"></script>
</body>

ChromeブラウザからSensorTagに接続する

以上のコンテンツをWebサーバに配置して、Chromeから開いてみましょう。
必ず、HTTPSでアクセスしてください。

image.png

SensorTagの電源ボタンを1回押して、緑色のLEDが点滅するのを確認して、「接続」ボタンを押下します。
そうすると、以下のようなデバイスの選択画面が出てきます。
以下の画面ではすでに1度接続しているときのものですが、初めて接続する場合には、不明なデバイスとして表示されるかもしれません。

image.png

対象デバイスを選択してから「ペア設定」ボタンを押下します。
数秒してからボタンが「切断」に変われば接続成功です。
各センサ名のチェックボックスをOnにすることで、1秒ごとに表示が変わるのがわかります。
Keysも、SensorTagの両脇のボタンを押すと表示が切り替わります。

image.png

以上です。

(2回目はLINE Thingsに対応させます。)
TI SensorTagをブラウザから使う(2):with LINE Things

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

【目的無しの泥臭調査⑥】教育用プログラミング言語「Blockly」に触れて、童心に帰って頭を再整理

  • ふと気になったことを、淡々と赴くままに調査していく、この上ない自己満足記事第六弾。
  • 「久々の泥臭調査は燃えたぎるな」という、極めて浅い歴史にも関わらず、老舗気分に酔いしれる、変わらない生活。
  • 今回は、様々なものに手を出しすぎている私の「頭の再整理・原点回帰」のために、教育用プログラミング言語の「Blockly」に触れていくことにしよう。

概要

  • 視覚的にプログラミングを学習することができる言語。
  • 画面上にある「ブロック」を、パズルのように組み合わせていくことで、プログラミング思考を学習することができる。
  • レゴブロックのように、楽しみながら学ぶことができるため、主に教育用として用いられている。
  • 作成したものを、「JavaScript・Python・PHP」等といった言語に変換することができる。
  • 公式サイト

利点

ゲーム感覚で学習することができる

  • 「画面操作のみ・ブロックを配置していくのみ」でプログラムを作成することができるため、難しい記述を覚える必要がない。
  • また、Blocklyを体験できるBlocklyGames等といったサイトが豊富にあるため、ゲームしている感覚で学習できる。

環境に依存しない

  • ブラウザがあれば、どこでも学習することができるほか、iOS版やAndroid版もあるため、環境に困らない。

様々な世代が利用できる

  • 「教育用としての子供向け言語」ではなく、下記のような様々な場面で広く利用できる。
    • 子供・・・レゴブロックで遊ぶ感覚で、幼少期のころからプログラミングに触れることができる。
    • 学生・・・学習を始める中高生や大学生が、必修化されるプログラミング授業でつまずくことなく、楽しみながら学べる。
    • 大人・・・学生同様に、初期学習におけるつまずきの緩和ができるほか、子供と一緒に遊びながら学ぶことができる。
    • エンジニア・・・既にエンジニアとして働いている新人等が、「基礎的な論理や仕組み」の理解や整理を図ることができる。

拡張性が高い

  • 標準のブロックだけではなく、自分で作成することもできるため、より複雑な論理を組むこともできる。

環境構築不要

同様の教育系プログラミングサービス

画面

  • 下記のようにブロックを組み合わせていきながら、ブラウザ上で学習することがきる。

image.png

  • また自分のローカル環境上に作成して、ブロックを拡張することができる。

image.png

image.png

手順

ブラウザエディタを利用する場合

image.png

  • 「タブメニュー」「カテゴリメニュー」が表示される。
  • 画面上の各機能の詳細は下記。

    • Blocks・・・配置するためのブロックが用意されている。
      • Logic・・・if等の論理部分
      • Loops・・・forやwhile等のループ部分
      • Math・・・数値部分
      • Text・・・出力部分
      • Lists・・・配列等のリスト部分
      • Colour・・・デザイン部分
      • Variables・・・変数部分
      • Functions・・・関数部分
    • JavaScript・・・ブロックをJavaScriptに変換
    • Python・・・ブロックをPythonに変換
    • PHP・・・ブロックをPHPに変換
    • Lua・・・ブロックをLuaに変換
    • Dart・・・ブロックをDartに変換
    • XML・・・ブロックをXMLに変換
  • 作成したいプログラムのイメージが固まったら、ブロックを配置していく。

image.png

  • 作成後、右上の「」ボタンで実行して、結果を確認する。

image.png

  • 完了。

自分のローカル環境で利用する場合

  • 下記のいずれかの方法で、自分のローカル環境にダウンロードする。

    • こちらのサイトにアクセスして、zipファイルをダウンロードする。
    • コマンドプロンプト(ターミナル)で下記のコマンドをうって、ダウンロードする。
      • git clone https://github.com/google/blockly.git
  • ダウンロードしてきたライブラリの「¥blockly¥demos¥code」の場所にある「index.html」を開く。

  • 開いたら、先程のブラウザエディタを利用する場合の部分で説明したブラウザエディタが表示されるため、こちらで学習を進めていく。

  • ブロック等を自分でカスタマイズしたい場合は、「¥blockly」の中に「index.html」を作成する。

  • 中身のコードを下記の例のようにする。

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <script src="blockly_compressed.js"></script>
    <script src="blocks_compressed.js"></script>
    <script src="msg/js/ja.js"></script>
</head>

<body>
    <div id="blocklyDiv" style="height: 480px; width: 600px;"></div>
    <xml id="toolbox" style="display: none">
        <category name="コントローラ" colour="210">
            <block type="controls_if"></block>
            <block type="controls_whileUntil"></block>
            <block type="controls_for">
        </category>
        <category name="ロジック" colour="210">
            <block type="logic_compare"></block>
            <block type="logic_operation"></block>
            <block type="logic_boolean"></block>
        </category>
        <category name="ループ" colour="120">
            <block type="controls_repeat_ext"></block>
        </category>
        <category name="テキスト" colour="220">
            <block type="text"></block>
            <block type="text_print"></block>
        </category>
        <category name="計算" colour="230">
            <block type="math_number"></block>
            <block type="math_arithmetic"></block>
            <block type="math_round"></block>
        </category>
        <category name="カラー" colour="20">
            <block type="colour_random"></block>
            <block type="colour_rgb"></block>
        </category>
    </xml>
    <script>
        var workspace = Blockly.inject('blocklyDiv', {
            toolbox: document.getElementById('toolbox'),
        });
    </script>
</body>

</html>
  • 記述後、ブラウザで「index.html」で開いて、画面が表示されれば完了
  • ※今回は調査や体験のため、詳細なカスタマイズ方法はこちらを参考にする。

image.png

まとめ

  • 今回はBlocklyの調査ということで、「このブロックはここに配置すれば突破できるのか」という、子供以上に熱中してしまいながら記事を書く。
  • Blocklyの豊富な拡張性という素晴らしい機能は後回しにして、ひたすら用意されたBlocklyGameのレベル上げに励む私は、技術者の定義をひたすら思考。
  • 「最新のツールやライブラリ・情報に触れすぎたことによる頭の中の複雑化」の再整理を今回の成果として、引き続きレベル上げに精進することにしよう。

参考

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

リンク遷移後でアコーディオンの開状態を保持する

リンク遷移後でアコーディオンの開状態を保持させたいが、単純にアコーディオンのみ実装すると閉状態に戻る。
これはリンクに限らず、リロードさせると閉状態に戻ってしまう。

解決するには、jQueryでcookieを用いる。
jquery.cookie.jsでcookieが使えるようになる。
使用するには、ダウンロードするか、CDNサイトのURLから引っ張ってくるかになる。
ダウンロードは下記から
jquery.cookie.js

今回はCDNサイトからURLを引っ張ってくるやり方で行う。

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>

まずHTMLのheadに部分にscriptタグでURLを引っ張ってくる。

application.html.erb
<!DOCTYPE html>
<html lang="ja">
  <head>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.js"></script>
  </head>
  <title>Cookieサンプル</title>
</html>

両方の方法で必ずjQueryのライブラリを読み込んでおく必要がある。

これで準備はオッケー。

jQueryで処理を書いていく。

test.js
 $(function(){
    if($.cookie("open-panel")){
      $("#treeview-menu").show();
    }
    $("#accordion").click(function() {
      if($.cookie("open-panel")){
        $("#treeview-menu").slideUp();
        $.removeCookie("open-panel" , { path: "/" });
      } else {
        $("#treeview-menu").slideDown();
        $.cookie("open-panel" , "open" , { expires: 1,  path: "/" });
      }
    return false;
    });
  });

htmlファイル

test.erb
<ul>
  <li class="treeview active">
    <a href="" class="accordion" id="accordion">サンプル一覧</a>
      <ul class="treeview-menu" id="treeview-menu" style="display:none">
        <% @samples.each do |sample| %>
          <li><%= link_to sample.name, sample_path %></li>
        <% end %>
     </ul>
  </li>
</ul>

これでサンプル一覧押し込みで、アコーディオンが開閉でき、リンク先遷移後でも開状態を保持できる。

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

Nuxt.jsで新規プロジェクトのlocalhost:3000にアクセスするまでの手順[pug][sass][fontawesome][googlefonts][normalize]

私がいつもNuxt.jsでプロジェクトを作るときの初期設定の手順がだんだん固定化されてきたので、まとめてみました。
pug,sass,fontawesome,googlefontsに加えて、normalize.scssも入った、個人的全部入りです。
自分でも使っていくつもりなので、更新されることもあるかと思います。

環境

  • Mojave 10.14.3(18D109)
  • node.js v10.11.0
  • yarn 1.10.1

手順

1. nuxt.jsでプロジェクト作成

$ yarn create nuxt-app test_nuxt_app

test_nuxt_app の部分には自分のプロジェクトの名前を入れて、適宜読み替えてください。
色々と設定項目出てきますが、とりあえずこんな感じにしておきます。
スクリーンショット 2019-03-24 11.01.42.png

実はこれでプロジェクト自体は完成です。
コマンド実行したディレクトリに test_nuxt_app という名前のディレクトリがあるので、移動して、サーバを起動させてみます。

$ cd test_nuxt_app
$ yarn dev

スクリーンショット 2019-03-24 11.18.41.png

起動するとターミナル上ではこんな感じです。
powerlineとかアイコンフォントとか、色々ツッコミどころはありますが、とにかく当初の目的である、 localhost:3000 にブラウザからアクセスしてみましょう。
ちなみに、2019年3月24日現在ではnuxt.jsの最新バージョンは2.5.1のようです。

FireShot Capture 005 - test_nuxt_app - localhost.png

簡単ですね。

2. git管理

余談です。
ここまでのプロジェクトをgitでリポジトリを作って管理しましょう。
新しくShoutaWATANABE/nuxt_test_appというリポジトリを作りました。
ここにコミット・プッシュします。

$ git add -A
$ git commit -m "[add] yarn devまで"
$ git remote add origin https://github.com/ShoutaWATANABE/nuxt_test_app.git
$ git push -u origin master

これで完了です。
FireShot Capture 006 - ShoutaWATANABE_nuxt_test_app - github.com.png

3. 各種パッケージのインストール

さて、本番はこれからです。
以下のコマンドでまとめてパッケージをインストールします。

$ yarn add -D pug@2.0.3 pug-plain-loader node-sass sass-loader @nuxtjs/style-resources
$ yarn add nuxt-webfontloader nuxt-fontawesome @fortawesome/fontawesome-svg-core @fortawesome/vue-fontawesome @fortawesome/free-solid-svg-icons @fortawesome/free-brands-svg-icons

インストールが無事に完了したら、 nuxt.config.js を以下のように編集します。
※デフォルトコメントアウトは除外してあります。

nuxt.config.js
import pkg from './package'

export default {
  mode: 'universal',
  head: {
    title: pkg.name,
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: pkg.description }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  },
  loading: { color: '#fff' },
  css: [{ src: '~/assets/scss/_normalize.scss', lang: 'scss' }], //normalize
  plugins: [{ src: '~/plugins/global-components.js', ssr: true }], //コンポーネントを一括読み込み
  modules: [
    ['nuxt-webfontloader'], //googlefonts
    'nuxt-fontawesome', //fontawesome
    '@nuxtjs/style-resources', //共通スタイルシート読み込み
    '@nuxtjs/axios',
    '@nuxtjs/pwa'
  ],
  webfontloader: { //googlefonts
    google: {
      families: ['Josefin+Sans']
    }
  }, //googlefontsここまで
  fontawesome: { //fontawesome
    imports: [
      {
        set: '@fortawesome/free-solid-svg-icons',
        icons: ['fas']
      },
      {
        set: '@fortawesome/free-brands-svg-icons',
        icons: ['fab']
      }
    ]
  }, //fontawesomeここまで
  axios: {
  },
  build: {
    extend(config, ctx) {
      if (ctx.isDev && ctx.isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  }
}

4. 各種ファイルの作成・編集

インストールしたパッケージが使えるように、それぞれ関係するファイルの作成・編集をします。

pages/index.vue

pages/index.vue
<template lang="pug">
  section.container
    div
      logo
      h1.title test_nuxt_app
      h2.subtitle My transcendent Nuxt.js project
      .links
        a.button--green(
          href="https://nuxtjs.org/"
          target="_blank"
        ) Documentation
        a.button--grey(
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
        )
          font-awesome-icon(:icon="['fab', 'github']")
          |GitHub
</template>

<style lang="scss" scoped>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Josefin Sans', 'Quicksand', 'Source Sans Pro', -apple-system,
    BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

plugins/global-components.js

plugins/global-components.js
import Vue from 'vue'

import Logo from '~/components/Logo.vue'

Vue.component('Logo', Logo)

assets/scss/_normalize.scss

assets/scss/_nomalize.scss
html {
  -webkit-text-size-adjust: 100%;
  overflow: auto;
  height: 100%;
  font-family: 'Avenir', 'Helvetica Neue', 'Helvetica', 'Arial', 'Hiragino Sans', 'ヒラギノ角ゴシック', YuGothic, 'Yu Gothic', 'メイリオ', Meiryo, 'MS Pゴシック', 'MS PGothic', sans-serif;
}

body {
  margin: 0;
  -webkit-font-smoothing: antialiased;
  line-height: 1.8;
}

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

hr {
  box-sizing: content-box;
  height: 0;
  overflow: visible;
}

pre {
  font-family: monospace, monospace;
  font-size: 1em;
}

a {
  background-color: transparent;
}

abbr[title] {
  border-bottom: none;
  text-decoration: underline;
  text-decoration: underline dotted;
}

b,
strong {
  font-weight: bolder;
}

code,
kbd,
samp {
  font-family: monospace, monospace;
  font-size: 1em;
}

small {
  font-size: 80%;
}

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

img {
  border-style: none;
}

button,
input,
optgroup,
select,
textarea {
  font-family: inherit;
  font-size: 100%;
  line-height: 1.15;
  margin: 0;
}

button,
input {
  overflow: visible;
}

button,
select {
  text-transform: none;
}

button,
[type="button"],
[type="reset"],
[type="submit"] {
  -webkit-appearance: button;
}

button::-moz-focus-inner,


[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

button:-moz-focusring,


[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

fieldset {
  padding: 0.35em 0.75em 0.625em;
}

legend {
  box-sizing: border-box;
  color: inherit;
  display: table;
  max-width: 100%;
  padding: 0;
  white-space: normal;
}

progress {
  vertical-align: baseline;
}

textarea {
  overflow: auto;
}

[type="checkbox"],
[type="radio"] {
  box-sizing: border-box;
  padding: 0;
}


[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

[type="search"] {
  -webkit-appearance: textfield;
  outline-offset: -2px;
}

[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

::-webkit-file-upload-button {
  -webkit-appearance: button;
  font: inherit;
}

details {
  display: block;
}

summary {
  display: list-item;
}

template {
  display: none;
}

[hidden] {
  display: none;
}

*,
*:after,
*:before {
  box-sizing: border-box;
}

サーバを再起動して、ブラウザの画面を確認してみましょう。
FireShot Capture 008 - test_nuxt_app - localhost.png

タイトルのフォントが変わって、line-heightも広くなっています。
logoのコンポーネントはindex.vueでは読み込みを明記していませんが、しっかりグローバルコンポーネントとして読み込まれています。
また、githubへのリンクにfontawesomeのアイコンフォントを追記したものも、きちんと表示されていますね。

スクリーンショット 2019-03-24 12.42.20.png

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

React Static 設定大全

Reactベースのイケてる静的サイトジェネレータ「React Static」の翻訳シリーズの続編です。

基礎概念編 https://qiita.com/IYA_UFO/items/b7946674a782374f7eb2

React Staticの殆どの設定はstatic.config.jsに書くことになり、こちらにツールとして学習が必要な内容が集中しています。
逆に言うと他はほぼReactです!

ということで、以下は公式ドキュメント「設定static.config.js」編の抄訳です。

MIT License
Copyright (c) 2013-present, Nozzle, Inc.
https://github.com/nozzle/react-static/blob/master/LICENSE


static.config.jsファイルの設置は任意ですが、React Staticの機能をフル活用するためには設置をおすすめします!
プロジェクトのルートに置き、1つのオブジェクトをdefault exportさせてください。
そのオブジェクトは、以下から必要なプロパティを持つことができます。

訳注
利用頻度が少なそうなプロパティについては訳していません。
完全なリストは本家へ https://github.com/nozzle/react-static/blob/master/docs/config.md

  • getRoutes
  • route
  • getSiteData
  • siteRoot
  • stagingSiteRoot
  • basePath
  • stagingBasePath
  • devBasePath
  • assetsPath
  • Document
  • webpack
  • paths

getRoutes

routeオブジェクトの配列をresolveする非同期の関数です。動的なデータや情報のリクエストは全てここに設定します。Booleanの引数devは、Productionビルド中であるかどうかを示します。

export default {
  getRoutes: async ({ dev }) => [...routes],
}

開発サーバー起動中にstatic.config.jsを更新した場合、getRoutesは自動で再度実行され、以下で説明するroutesrouteDataへの変更はhot reloadされます。
以下ではこのプロパティで扱うrouteオブジェクトについて解説します。

route

routeはサイトの特定のパスを示すオブジェクト。React staticサイトの屋台骨となる設定。
プロパティは以下

path...文字列

このルートにマッチするパス。siteRoot + basePathからの相対パスで、アンカーリンクやクエリパラメータは除いたもの。
子ルートの場合は、親ルートからの相対パス。

component...文字列

このルートをレンダリングするためのコンポーネントへのパス。
プロジェクトルートからの相対パス。

getData...非同期関数

(resolvedRoute, { dev }) => Object
このルートに必要なデータの配列をreturnまたはresolveする非同期関数。
引数は

  • resolvedRoute...オブジェクト この関数が扱うルート
  • flags...オブジェクト ビルドについてのメタ情報やフラグが入っている
  • dev...真偽値 開発環境か本番ビルドかを示す

children...routeオブジェクトの配列

配下の子ルート。パスは親から引き継がれるので、この中でprefix部分を繰り返す必要はない。

redirect...URL

セットすると、http-equivを利用した301リダイレクトと同じ挙動をし、ページはリダイレクト以外何もしない最低限のHTMLになる。

noindex...真偽値

デフォルトでfalse。このルートと子ルートを自動生成されるsitemap.xmlに含みたくない場合はtrue

例:

export default {
  getRoutes: async ({ dev }) => [
    // A simple route
    {
      path: 'about',
      component: 'src/containers/About',
    },

    // A route with data
    {
      path: 'portfolio',
      component: 'src/containers/Portfolio',
      getData: async () => ({
        portfolio,
      }),
    },

    // A route with data and dynamically generated child routes
    {
      path: 'blog',
      component: 'src/containers/Blog',
      getData: async () => ({
        posts,
      }),
      children: posts.map(post => ({
        path: `post/${post.slug}`,
        component: 'src/containers/BlogPost',
        getData: async () => ({
          post,
        }),
      })),
    },

    // A 404 component
    {
      path: '404',
      component: 'src/containers/NotFound',
    },
  ],
}

getSiteData

getSiteDataはルートのgetData関数に似ていますが、結果はSiteDataコンポーネントとgetSiteDataHOCによってサイト全体で利用できます。ここから返されるデータはセッションに1回だけロードされますが、全てのページに埋め込まれます。慎重に使ってください。

例:

export default {
  getSiteData: async ({ dev }) => ({
    title: 'My Awesome Website',
    lastBuilt: Date.now(),
  }),
}

siteRoot

protocol://domain.comの形で設定します。SEO対策のため、設定を強くおすすめします。
この設定により、以下が起きます

  • 自動でsitemap.xmlを出力します
  • 静的なリンクを絶対パスに変換します。サイトがhttpsで配信される場合、こちらに必ず書いてください。リンクのパス末尾のスラッシュは自動的に削除されます。ベース・パスを設定する(例えばGithub Pagesを使っている場合)には、ここではなく、bathPathオプションを使ってください。

例:

export default {
  siteRoot: 'https://mysite.com',
}

stagingSiteRoot

siteRootと同じ機能ですが --stagingフラグがついている場合のみ有効になります。

basePath

some/routeの形で書きます。 ドメインのルート以外でサイトをホストする場合に使います。例えばGitHub Pagesでホストする場合、サイトはhttps://mysite.com/blogでホストされるので、basePathblogとなります。

例:

export default {
  basePath: 'blog',
}

stagingBasePath

basePathと同じ機能ですが、--stagingフラグがついている場合のみ有効になります。

devBasePath

basePathと同じ機能ですが、ローカル開発サーバーでのみ有効になります。

assetsPath

JSとCSSのロード先を指定します。アセットをCDNのような外部サービスでホストする場合に使えます。

Document

サイトのルートをレンダリングするコンポーネントです。ルートドキュメントのカスタマイズはかつてなく簡単になりました。
このコンポーネントには以下のようなことが書けるでしょう。

  • headmetaタグの内容
  • サイト全体の計測スクリプト
  • サイト全体に適用するスタイルシート

以下のpropsが与えられています。

Html...Reactコンポーネント

htmlタグ

Head...Reactコンポーネント

headタグ

Body...Reactコンポーネント

htmlタグ

body...Reactコンポーネント

bodyタグ

children...Reactコンポーネント

サイトのメインコンテンツ。レイアウトやルートを含みます。

routeInfo...オブジェクト

routeDataを含む、現在のルートの情報。

siteData...オブジェクト

この設定ファイルのgetSiteDataから渡される全てのデータ。

renderMeta...オブジェクト

hooktransformerでレンダリング中にセットされるデータです。

例:

export default {
  Document: ({ Html, Head, Body, children, siteData, renderMeta }) => (
    <Html lang="en-US">
      <Head>
        <meta charSet="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </Head>
      <Body>{children}</Body>
    </Html>
  ),
}

JSXを使うことになるので、この場合はstatic.config.jsの先頭でimport React from 'react'してください。

devServer

開発サーバーとして使われるwebpack-dev-serverインスタンスに渡されるオプションオブジェクトです。

例:

export default {
  // An optional object for customizing the options for the
  devServer: {
    port: 3000,
    host: '127.0.0.1',
  },
}

paths

React staticが使う内部的なファイルパスをカスタマイズするオブジェクトです。全てプロジェクトルートからの相対パスです。

デフォルトは以下:

export default {
  paths: {
    root: process.cwd(), // プロジェクトのルート。よく分からなければ変更しないでください。
    src: 'src', // ソースフォルダ。index.jsをエントリーポイントとして含む必要あり。
    temp: 'tmp', // ビルドで発生する公開しないファイルの置き場。
    dist: 'dist', // 本番用出力フォルダ。
    devDist: 'tmp/dev-server', // The development scratch directory.
    public: 'public', // The public directory (files copied to dist during build)
    assets: 'dist', // The output directory for bundled JS and CSS
    buildArtifacts: 'artifacts', // The output directory for generated (internal) resources
  },
}

続編 React Static 設定大全 https://qiita.com/IYA_UFO/items/b01ca2eb1ec0082c4b79

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

immerで複雑な構造のオブジェクトを扱う

複雑な構造のオブジェクトに何か値を追加する際、現在の構造を壊さずに値を追加しなければならないのでスプレッド演算子をたくさん使ったコードになりがちです。

イメージとしてはこんな感じのやつです。

const user; // 変更前のユーザー。複雑な構造をしている
const additionalHoge = ['hoge', 'huga']; // 追加したいデータ

const newUser = {
  ...user,
  detail: {
    ...user.detail,
    hoge: [...user.detail.hoge, ...additionalHoge],
  }
}

こういう雰囲気のコードをいい感じにリファクタできるimmerというライブラリを最近知りました。mobxの作者の方が作ったライブラリみたいです。

immerを使うと上のやつはこんな感じで書き直せます。
元の変数 user には変更はなく、新たに変更が反映されたオブジェクト newUser が返ってきます。
元の構造を維持するための ...user みたいなコードは一掃出来るのでどこに変化があるのかがわかりやすいです。

import produce from 'immer';

const user; // 変更前のユーザー
const additionalHoge = ['hoge', 'huga']; // 追加したいデータ

const newUser = produce(user, draftUser => {
  draftUser.detail.hoge = [...draftUser.detail.hoge, ...additionalHoge];
});

これくらいの構造のネストであれば、そのままjsで書いてもいいと思いますが、ネストが何重にもなればなるほど、immerの見やすさが顕著に出てくると思います。

使いどころとしてはreduxのreducerで使うような例が公式のreadmeにものってます。
他に使えそうと思ったところとして、react-apolloでreact-infinite-scrollerを用いた無限スクロールを行う際、元のデータにデータを追加するコードがスプレッド演算子だらけになるので、ここでも使えるなあと思いました。

react-apollのこういう感じのコードを綺麗に出来る。参考

          return {
            ...previousResult,
            search: {
              ...previousSearch,
              nodes: [...previousNodes, ...currentNodes],
              pageInfo: currentSearch.pageInfo,
            },
          };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphQL APIをVueJSフロントエンドに接続する方法

この記事はMarion Schleiferのこの記事を翻訳したものです
How to connect your GraphQL API to your VueJS Frontend
I recently wrote a blogpost about how to create an API with Hasura’s GraphQL engine. If you are not familiar with…medium.com

私は最近、HasuraのGraphQLエンジンを使ってAPIを作成する方法についてのブログ記事を書きました。 GraphQLにまだ慣れていない方は、まずこの記事を読むことをお勧めします。それでは、非常にシンプルなVueアプリを作成し、それをGraphQL APIに接続する方法を学びましょう。

Vueアプリの作成中に何か不明な点がある場合、または完全な解決策を見たい場合は、いつでもこのプロジェクトをGithubで見ることができます。

目次
Vueプロジェクトを作成する
映画リストコンポーネント
映画詳細コンポーネント
ムービーコンポーネントを追加する
GraphQL APIへの接続
GraphQL APIから実際のデータを取得する
映画リスト
映画を追加
この後は?
コミュニティに参加する

Vueプロジェクトを作成する

vue create harry-potter-app

デフォルトのプリセットを選択して、お気に入りのエディタでプロジェクトを開きます。アプリの外観を少しきれいにするために、Milligramライブラリを追加します。 index.html内に

の内側に次の3行を追加します。
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">

<link rel="stylesheet" href="//cdn.rawgit.com/necolas/normalize.css/master/normalize.css">

<link rel="stylesheet" href="//cdn.rawgit.com/milligram/milligram/master/dist/milligram.min.css">

端末に移動し、プロジェクトにcdして、次のようにローカルに入力してアプリケーションを実行します。

yarnの場合は:

yarn serve

Npmの場合は

npm run serve

Vueのロゴが表示されたVueのHello Worldページが表示されます。

前回の記事で作成したHarry Potter APIを見ると、movies、characters、actors、scenesといういくつかのテーブルがあることがわかります。私たちのVueアプリでしたいことは、映画用のコンポーネントを作成することです。最後に、1つのmovieを表す複数のコンポーネントで構成されるmovieのリスト用のコンポーネントが必要です。そしてmovieを追加することを可能にするコンポーネントを作成します。

Movieリストコンポーネント

始めましょう。まず、MovieListコンポーネントをApp.vueファイルに登録します。ファイル内で、コンポーネントをに置き換える必要があります。いまはまだ失敗します。新しいコンポーネントを登録していないからです。コンポーネントリストの同じファイルのもう少し下の部分で、名前を変更したコンポーネントを正しい場所からインポートし、HelloWorldコンポーネントをMoviesListコンポーネントに置き換えます。完了したら、次のようになります。

それでは、HelloWorld.vueファイルの名前をMoviesList.vueに変更する必要があります。ファイル内で、<template><script>を置き換えて、次のようになります。

ここで何が起こっているかを説明します。テンプレートの内側に、v-forを使ってすべてのmovieを反復処理する<div>を追加します。各movieの識別方法をVueに指示する必要があります。各movieの識別方法は、そのmovieのIDで行います。二重の中括弧を使用して、ブラウザに何を表示するかをVueに指示します。これは、この場合はmovieのIDです。 <script>タグ内に、表示するデータを定義します。今のところ、定義したIDを持つmovieであるダミーデータを表示します。ブラウザでこのアプリを見ると、VueのロゴとmovieのIDが表示されます。

Movie詳細コンポーネント

前述したように、1つのmovieを表示するためのコンポーネントも必要です。これの目的は、ダミーデータを使用する代わりに、実際のMovieItemのコレクションをMoviesListに後で表示できるようにすることです。それでは、componentsフォルダの中を進んで、MovieItem.vueという新しいファイルを作成しましょう。 MoviesListの各MovieItemについて、title、director、composer、およrelease dateを表示します。それでは、先に進み、コンポーネントをMovieItem.vueファイルに追加しましょう。

そのためには、単純な

の代わりにコンポーネントを使用するように3行目を変更する必要があります。また、8行目で、対応するファイルからMovieItemをインポートする必要があります。最後に、11行目で、MovieItemが現在のファイル内でmovieのリストのインスタンスとして使用されるコンポーネントであることを説明する必要があります。

ブラウザをリロードすると、次のようになります。

movieコンポーネントの追加

それでは、新しいmovieをアプリケーションに追加する機能を追加しましょう。そのためには、componentsフォルダの中に、AddMovie.vueという新しいファイルを作成し、今のところ空のままにします。今度はApp.vueを開き、<template>内で、<movies-list>コンポーネントの前に<add-movie />コンポーネントを追加します。また、MoviesListコンポーネントと同様に、正しい場所からコンポーネントをインポートし、AddMoviesコンポーネントをコンポーネントリストに追加する必要があります。完了したら、<template><script>は次のようになります。

AddMovie.vueファイル内に、新しいムービーを作成するためのフォームを追加します。 APIを見ると、movieテーブルにどのようなフィールドがあるのか​​がわかります。今のところ、私たちはタイトル、監督、作曲家そしてリリース日を持っている映画を作成することができたいです。ファイルは次のようになります。

これはmovieのフィールドを入力して送信することを可能にする非常に単純なフォームです。まだコンポーネントにメソッドを追加していないので、送信ボタンをクリックしても何も起こりません。

ブラウザをリロードすると、次のようになります。

GraphQL APIへの接続

次のステップは、前回の投稿で作成したGraphQL APIにVueアプリを接続して、ダミーデータの代わりに実際のデータを表示できるようにすることです。それでは始めましょう。

前回の記事では、GraphiQLツールでクエリを実行する方法を説明しました。 Hasuraコンソールに移動してGraphiQLで、すべてのムービーのリストを取得するためのメソッドと、新しいムービーを追加するための機能をテストしましょう。

さて、Vueプロジェクトから、私たちはまさにこれらのメソッドを呼び出したいです。そのためには、Apolloクライアントを使います。 Apolloは、簡単な言語であなたのAPIに問い合わせをすることを可能にするGraphQLプラットフォームです。プロジェクトフォルダのコマンドラインで次のコマンドを実行して、Apolloをインストールしましょう。

npmの場合:

npm install apollo-client apollo-cache-inmemory apollo-link-http graphql-tag graphql --save

yarnの場合:

yarn add vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-cache-inmemory graphql-tag

まず、Hasuraプロジェクトへのリンクを確立し、Apolloをインスタンス化する必要があります。 main.jsを次のように変更します。

これを見てみましょう。最初に、インストールしたばかりのライブラリをインポートする必要があります。それらをこのファイルで使用するからです。 HttpLinkを使用して、12〜14行目でGraphQL APIへの接続を確立します。 HasuraプロジェクトのGraphiQLツールに独自のカスタムリンクがあります。

次に、16行目から20行目で、ApolloClientのインスタンスを作成します。引数として、HttpLinkを渡して、データが正しいAPIからポーリングされるようにします。次にキャッシュを渡します。 InMemoryCacheはApolloClientのデフォルトのキャッシュ実装であるため、これを使用します。最後に開発ツールに接続するためのオプションを渡します。そのため、デバッグが必要な場合に備えて、クロムインスペクタにApolloタブが表示されます。

22行目では、VueインスタンスをVueApolloを使用するように設定しています。

Vueアプリからクエリやmutationを作成できるようにするには、ApolloProviderのインスタンスを作成する必要があります(24〜26行目)。 Apolloクライアントに渡して、これらの操作をコミットします。

最後に、28行目から32行目で、Apolloプロバイダーが渡されるVueアプリを起動し、アプリ全体でデータベースのクエリと操作を行えるようにします。

GraphQL APIからデータを取得する

これまでのところ、Vueフロントエンドにダミーデータを表示しています。さて、実際のデータを表示したいです。 MoviesListには、現在データベースに保存されているmovieを表示します。また、映画をデータベースに保存してフロントエンドに表示できるように、movieを追加できるようにしたいと考えています。

Moviesリスト

データベースから映画を表示するには、MoviesList.vueを次のように変更しましょう。

<script>タグの中に、GraphQLをインポートする必要があります。これにより、クエリを実行できるようになります。 11〜21行目では、映画のリストを取得するためのGraphQLクエリを定義しています。

デフォルトのexport default関数内で、ダミーデータを削除し、データベースからポーリングした映画のリストを返します。また、ApolloにどのクエリをAPIに送るべきかを伝える必要があります。そして先に定義したGET_MOVIESクエリを渡します。

ブラウザを更新すると、データベースから映画の全リストを見ることができます。

やったできました!

movieの追加

これまでのところ、フォームを送信しても何も起こりません。私たちは今、私たちのフォームの上に映画を追加できるようにしたいです。これには、AddMovie.vueファイルにいくつかの変更を加える必要があります。

繰り返しになりますが、<script>タグの中に、GraphQLをインポートする必要があります。また、InMemoryCacheをインポートする必要があります。その理由はほんの少しのうちにわかります。

17〜39行目で、データベースに映画を追加するためのmutationを定義します。まず、渡されるパラメータを定義します。次に、movieを挿入するためのオブジェクトとして渡します。そして第三にmutationの戻り値が先程作成したmovieのidであることを記述します。

export default関数内の最も重要な変更は52〜67行目に反映されており、送信ボタンがクリックされたときの動作を定義しています。まず、ボタンをクリックしたときに送信されるフォームのデフォルトの動作を防ぐ必要があります。次にmutationに渡すデータを定義します。次に、$ apolloインスタンスへのmutationを呼び出し、オブジェクト作成のために変数を渡します。最後に、movieリストを更新する必要があります。新しく追加されたmovieがリストに表示されるようにブラウザをリロードしたくないためです。これにはキャッシュが必要です。以前にmovieをリストするためにクエリを実行したので、キャッシュからそれを再フェッチすることができます。

この後は?

よくできました!これで小さなVueアプリを作成しそれをGraphQL APIに接続しました。これはとても簡単でしたね。しかし、これはほんの始まりです!我々のアプリに追加できるもっとたくさんの機能があります。たとえば、映画の更新や削除などです。または各映画のキャラクターとそれに対応する俳優を表示します。 Hasuraのドキュメントを調べてください。このアプリを拡張するために必要なものすべてを見つけることができます。

コミュニティに参加しましょう

Hasuraはますます多くの開発者によって使用されています。フレンドリーなコミュニティに参加して、最新情報を入手してください。

Hasuraであなた自身のプロジェクトを構築したい場合は、ここからdiscordのチャンネルに参加することができます。すでにかなり大きなコミュニティがあり、あなたはすぐに助けを得るでしょう。

?Twitter

️⭐️Github

Mediumの元の記事

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

nodebrewでインストールしたpm2をRasbperry Piで自動起動させる

ラズパイ起動と共にpm2でスクリプト起動の記事をnodebrewでインストールした環境でやってみました。

流れ

  • 永続化させるプロセスをpm2で起動
  • pm2 startupを実行
    • ヒントっぽいのがでる
    • 無視して↓のsudo env~を実行
  • pm2 saveで保存

まずは永続化

pm2 start app.js

ここで永続化したプロセスが↓の手順で記憶される模様です。

pm2 startup

$ pm2 startup

色々でるけどスルーして↓の行を実行。
ここの行が全てな気がした。

sudo env PATH=$PATH:/home/pi/.nodebrew/current/bin /home/pi/.nodebrew/current/bin/pm2 startup systemd -u pi --hp /home/pi

ちなみにpm2 startupすると出てくるsudo env PATH=$PATH:/home/pi/.nodebrew/node/v11.6.0/bin /home/pi/.nodebrew/node/v11.6.0/lib/node_modules/pm2/bin/pm2 startup systemd -u pi --hp /home/piなどのコマンドはバージョンがv11.6.0みたいに固定で書いてあるので、これをコピペしてしまうとNode.jsのバージョンが変わってしまったら対応できなくなってしまいそう

↓こんな感じのログが出ます。利用しているcurrentバージョン(↓v11.6.0)のパスを自動的に設定してくれてますね。

[PM2] Init System found: systemd
Platform systemd
Template
[Unit]
Description=PM2 process manager
Documentation=https://pm2.keymetrics.io/
After=network.target

[Service]
Type=forking
User=pi
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
Environment=PATH=/home/pi/.nodebrew/node/v11.6.0/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Environment=PM2_HOME=/home/pi/.pm2
PIDFile=/home/pi/.pm2/pm2.pid

ExecStart=/home/pi/.nodebrew/node/v11.6.0/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/home/pi/.nodebrew/node/v11.6.0/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/home/pi/.nodebrew/node/v11.6.0/lib/node_modules/pm2/bin/pm2 kill

[Install]
WantedBy=multi-user.target

Target path
/etc/systemd/system/pm2-pi.service
Command list
[ 'systemctl enable pm2-pi' ]
[PM2] Writing init configuration in /etc/systemd/system/pm2-pi.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-pi...
[PM2] [v] Command successfully executed.
+---------------------------------------+
[PM2] Freeze a process list on reboot via:
$ pm2 save

[PM2] Remove init script via:
$ pm2 unstartup systemd

保存

$ pm2 save

再起動して確認

  • 再起動
$ sudo reboot
  • 再起動後に確認するとappのプロセスが立ち上がってます。
$pm2 list

┌──────────┬────┬─────────┬──────┬─────┬────────┬─────────┬────────┬──────┬───────────┬──────┬──────────┐
│ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu  │ mem       │ user │ watching │
├──────────┼────┼─────────┼──────┼─────┼────────┼─────────┼────────┼──────┼───────────┼──────┼──────────┤
│ app      │ 0  │ N/A     │ fork │ 878 │ online │ 1       │ 67s    │ 0.2% │ 37.5 MB   │ pi   │ disabled │
└──────────┴────┴─────────┴──────┴─────┴────────┴─────────┴────────┴──────┴───────────┴──────┴──────────┘

自動起動を解除する

自動起動に設定したスクリプトが悪さしてるみたいで、一旦解除したくなりました。

pm2 unstartup systemd

さっきとほぼ一緒ですが、~~ unstartup sytemd ~~という形で unstartupになってるので注意しましょう。

sudo env PATH=$PATH:/home/pi/.nodebrew/current/bin /home/pi/.nodebrew/current/bin/pm2 unstartup systemd -u pi --hp /home/pi

ここもstartupの箇所がunstartupに

所感

Pythonのスクリプトは/etc/rc.localとかでやってたので新鮮。

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