- 投稿日:2019-12-24T23:34:20+09:00
年末まで毎日webサイトを作り続ける大学生 〜67日目 オブジェクト指向で�手紙を作る〜
はじめに
こんにちは!@70days_jsです。
今日はオブジェクト指向で手紙を作りました。(gif)↓
クリックすると次の手紙が表示されます。
今日は67日目。(2019/12/24)
よろしくお願いします。サイトURL
https://sin2cos21.github.io/day67.html
やったこと
オブジェクト指向で手紙を作りました。
html↓
<body> <h2>Letters</h2> </body>css↓
body { font-family: Helvetica, YuGothic, "游ゴシック", sans-serif; } .letter { display: flex; justify-content: flex-start; align-items: center; padding: 20px; height: 200px; width: 300px; border: solid 1px black; position: absolute; top: 50%; left: 50%; background-color: white; transform: translateX(-50%) translateY(-50%); } .pageShift { display: none; }JavaScript↓
let letters = [], name = [ "太郎", "二郎", "三郎", "四郎", "五郎", "六郎", "七郎", "八郎", "九郎", "十郎" ], event = [ "ラグビー", "サッカー", "ゲーム", "サバイバル", "ナルト", "ドラゴンボール", "宇宙", "犬", "鳥", "ニンジン" ], numberOfSheet = 10; function Letter(name) { this.name = name; } Letter.prototype.head = function() { this.head = "拝啓<br>"; }; Letter.prototype.main = function(event) { this.main = "お久しぶりです。" + this.name + "です。<br>実は最近" + event + "をしまして、その結果を報告しようと思った次第でございます。<br>"; }; Letter.prototype.foot = function() { this.foot = "敬具"; }; for (var i = 0; i < numberOfSheet; i++) { let letter = new Letter(name[i]); letters.push(letter); let div = document.createElement("div"); div.classList.add("letter"); div.setAttribute("id", i); let color = randomRGB(); div.style.backgroundColor = color; letter.head(); letter.main(event[i]); letter.foot(); div.innerHTML = letter.head + letter.main + letter.foot; document.body.appendChild(div); let eveLis = document.getElementById(i); eveLis.addEventListener("click", function() { eveLis.classList.add("pageShift"); }); } function randomRGB() { let r = Math.floor(Math.random() * 256); let g = Math.floor(Math.random() * 256); let b = Math.floor(Math.random() * 256); let rgb = "rgba(" + r + ", " + g + "," + b + ", 1)"; return rgb; }今回手紙で内容が変わっている部分は、名前とイベントの2つです。
これはあらかじめ配列に用意しておきました。LetterオブジェクトはLetterコンストラクターを使って作ります。
メソッドはhead, main, footです。
それぞれ手紙の冒頭、内容、締めの言葉を担当しています。手紙の変更はイベントリスナーでclickをつけて実現しています。↓
eveLis.addEventListener("click", function() {
eveLis.classList.add("pageShift");
});.pageShiftクラスはdisplayを消すクラスです。↓
.pageShift {
display: none;
}これで一番上にある手紙はクリックされると消えたように見えます。
感想
うまい具合の文章を考えてくれるプログラムを組んでみたい・・・。
機械学習の方向を勉強するしかないんだろうか・・・?最後まで読んでいただきありがとうございます。明日も投稿しますのでよろしくお願いします。
- 投稿日:2019-12-24T23:33:48+09:00
プロトタイプベースのオブジェクト指向とJavaScriptのプロトタイプ
社内勉強会の資料
概要
JSのプロトタイプについての理解と、その周辺の登場人物について理解する回。
もう過去何億人がこの話題について人に説明したかわからない程にはjsの基本のキですが、自分なりの解釈と自分なりの説明を書いてみます。
ここでの説明が理解できれば、MDNのプロトタイプに関する記述はすんなり読めるようになります。
メインの対象者としては、「jsは普通に書いてるしプロトタイプもなんとなく知ってるけど、説明しろって言われるとちょっと言葉に詰まる」という方です。
登場人物(tl;dr)
- コンストラクタ
new
演算しと共に使用される、オブジェクトを生成するための処理を書いた関数。.prototype
- コンストラクタオブジェクトが持つ、コンストラクタ(自身)のプロトタイプへの参照を持つプロパティ。
[[prototype]]
- ECMAScriptの仕様において、インスタンスのプロトタイプへの参照を持つ内部プロパティ。
- privateなプロパティなので、jsコード上で参照することは出来ない。
.__proto__
- ECMAScriptには元々定義されていなかったが、ChromeやFireFoxなどには実装されている。
- インスタンスに存在する、コンストラクタのプロトタイプへの参照を持つプロパティ。
[[prototype]]
と同じだが、こちらはjsコード上からアクセス可能。- ES6から初めて仕様として定義されたが、同時に非推奨となっている。
Object.getPrototypeOf()
- オブジェクトのプロトタイプを取得する関数。
- ES6から正式に仕様に存在し、
__proto__
に代わって推奨されるプロトタイプへのアクセス方法。Object.setPrototypeOf()
- オブジェクトのプロトタイプを代入する関数。
- ES6から正式に仕様に存在し、
__proto__
に代わって推奨されるプロトタイプへのアクセス方法。new
- コンストラクタから新しいオブジェクトを作る演算子。
class
- プロトタイプベース継承のシンタックスシュガー。
- クラスベースのオブジェクト指向におけるクラスとは本質的には別物。
流れ
説明の流れとしては、
- プロトタイプベースのオブジェクト指向の概念の理解
- プロトタイプをふんわりとコードで理解
- プロトタイプを実際のコードで理解
という感じで進めます。
プロトタイプベースのオブジェクト指向って?
オブジェクト指向 - wiki
The Early History Of Smalltalk1, EverythingIsAnObject.
2, Objects communicate by sending and receiving messages (in terms of objects).
3, Objects have their own memory (in terms of objects).
4, Every object is an instance of a class (which must be an object).
5, The class holds the shared behavior for its instances (in the form of objects in a program list).
6, To eval a program list, control is passed to the first object and the remainder is treated as its message.
— Alan Kay1, すべてはオブジェクトである。
2, オブジェクトはメッセージの受け答えによってコミュニケーションする。
3, オブジェクトは自身のメモリーを持つ。
4, どのオブジェクトもクラスのインスタンスであり、クラスもまたオブジェクトである。
5, クラスはその全インスタンスの為の共有動作を持つ。インスタンスはプログラムにおけるオブジェクトの形態である。
6, プログラム実行時は、制御は最初のオブジェクトに渡され、残りはそのメッセージとして扱われる。全てがクラスベースであるという前提は時に物事を複雑化してしまう。
例えばメソッドが必ず何らかのクラスに所属するという前提は強すぎる場合がある。クラスベースでは委譲や代理(プロキシ)によって動作にバリエーションを与えるが、初めからバリエーションをもったインスタンスを作成できればそのような機構は必要ない。
またインスタンス変数とメソッドの違いとは何か、という問題もある。C++やJavaのpublicなメンバ変数(フィールド)などを別にすれば、インスタンス変数とアクセサメソッドはほとんど等価の概念である。
クラスが存在しない世界ではテンプレート処理によるインスタンス化ができないため、新しいオブジェクトは完全に空の状態か、または他のオブジェクトの複製(クローン)によって作成される。プロトタイプベースでの継承は一般にこのクローンによる特性の引き継ぎを指す。
プロトタイプベースのオブジェクト指向とは、クラスベースのオブジェクト指向とは違ったアプローチでオブジェクト指向を実装したもの。
クラスベースのオブジェクト指向は、クラスという共通の挙動を定義したオブジェクトをテンプレートとして、その挙動を共通に持つ新しいオブジェクトを複数作ることができる。
それに対して、プロトタイプベースのオブジェクト指向にはクラスが存在せず、プロトタイプと呼ばれる共通の挙動を格納したオブジェクトへの参照を持つことで、オブジェクトは共通の挙動を得る。(この辺は言語によって細かい思想が違うので、一概に同じ言葉で説明出来ない部分もあると思うけど、概念的にはこう)
つまり、どちらにせよ
1, すべてはオブジェクトである。
2, オブジェクトはメッセージの受け答えによってコミュニケーションする。
3, オブジェクトは自身のメモリーを持つ。これらの思想は受け継いだうえで、オブジェクト間で共通の挙動を、どう定義するかの思想が異なっている。
つまり、プロトタイプベースのオブジェクト指向とは、「複数のオブジェクトに共通の挙動を持たせる方法として、クラスを使用せず、プロトタイプを使用するオブジェクト指向」のこと。
改めてプロトタイプ
したがって、プロトタイプとは、クラスとは違う方法で、複数のオブジェクトに共通の挙動を与える仕組みのこと。
主な意味 原型、模範、原形 プロトタイプと聞くと、「何かを作る際にとりあえず作るもの」というようなイメージが日本語的には強いですが、プロトタイプベースのオブジェクト指向における「プロトタイプ」は「原型」という意味で捉える方がしっくり来ます。「ひな形」「テンプレート」とも言えるでしょう。
オブジェクト指向において、オブジェクトに対して共通の振る舞いを持たせることを「継承」と呼びますが、これはプロトタイプベースであっても同じです。
jsにおけるプロトタイプの実装
ここまではプロトタイプベースのオブジェクト指向の概念でした。
ここからは、具体的にjsにおけるプロトタイプベースのオブジェクト指向の実装を見ていきます。
どうやって、プロトタイプが複数のオブジェクトに共通の挙動を持たせているか、という話です。複数のオブジェクトに共通の挙動を持たせる方法を雰囲気で理解
さて、jsにはクラスが存在しませんが、複数のオブジェクトに共通の挙動を持たせたい場面はプログラミングをしていると頻出します。
ここでは、わかりやすいようにプロトタイプの実装をプロトタイプを使わずにふんわり再現してみようと思います。
ここのコードは雰囲気なので実際には意図した挙動にならないものを含んでいます。
雰囲気を感じ取ってください()例えば、5つの異なるオブジェクトが存在するとして、
const obj1 = { hoge: "HOGE" }; const obj2 = { piyo: "PIYO" }; const obj3 = { num: 123 }; const obj4 = { fuga: "fuga" }; const obj5 = { f: () => {} };このオブジェクトたちを、文字列に変えたいとしましょう。
ログに吐きたいんです。名前は、toString
とでもしましょう。全部に同じ処理を持たせようと思ったら、
const toString = () => { // 文字列化する処理 }; obj1.toString = toString; obj2.toString = toString; obj3.toString = toString; obj4.toString = toString; obj5.toString = toString;こんな感じでどうでしょう。
あれ、オブジェクトをJSON形式にする、
toJSON
も入れたいですか?
一個ずつ代入しても良いですが、数が増えるとツラそうなので、一つのオブジェクトにして、全部にそれを渡しますか。const commonFunctons = { toString: () => { /* 文字列化する処理 */ }, toJSON: () => { /* JSON形式に変換する処理 */ }, }; // 変更されたくないので、触んないでね!という意味で、両はしをアンダーバー二つとかで囲っときましょう obj1.__commonFunctions__ = commonFunctions; obj2.__commonFunctions__ = commonFunctions; obj3.__commonFunctions__ = commonFunctions; obj4.__commonFunctions__ = commonFunctions; obj5.__commonFunctions__ = commonFunctions;こうすれば、複数のオブジェクトに同じ挙動を持たせることが出来そうですね。
プロトタイプの基本的な方針は、こういう感じです。ここで言う
commonFunctions
がプロトタイプと呼ばれます。
共通の挙動をするオブジェクトたちの「原型」です。ちなみにこれ、呼び出す時に
obj5.__commonFunctions__.toString();こうなりますが、うーん、
obj5.toString();こう呼び出したいですよね。
__commonFunctions__
は、もうルールとして、省略できるということにしましょう。この、呼び出す際にプロトタイプの参照を省略して良い仕組みを、プロトタイプチェーンと呼びます。
__commonFunctions__
は、実際のjsで言う__proto__
に相当します。
__proto__
は、そのインスタンスオブジェクトが参照しているプロトタイプへの参照です。
上の例では、commonFunctions
オブジェクトへの参照ですね。
__proto__
はブラウザなどが勝手に実装しているプロパティでしたが、ES6から初めて仕様として定義されました。
が、同時に非推奨となっています。
(推奨の方法としてgetPrototypeOf
やsetPrototypeOf
が正式に定義されましたが、このページでは解説のしやすさと理解しやすさから__proto__
を使います。)実際のprototypeの仕様
雰囲気を掴んだところで、本当のjsのprototypeの仕様を見ていきます。
ちなみに、関数もオブジェクト
jsはオブジェクト指向なので、だいたいのものがオブジェクトです。
さらに、jsでは関数が第1級オブジェクトなので、関数もオブジェクトです。
つまり、関数自体もプロパティを持ちます。
これを認識しておかないとプロトタイプの話においては結構混乱します。function doSomething() { console.log("something"); } doSomething.hoge = "hoge"; // 問題なく通るプロトタイプの確認
さて、わかりやすいように、具体的なオブジェクトを使いましょう。
const date1 = new Date(); const date2 = new Date(); const date3 = new Date();
Date
を使って新たにオブジェクトを3つ作りました。先ほどの例で言えば、この3つのdateオブジェクトは、共通の挙動を持った一つのオブジェクト(何度も言いますが、これをプロトタイプと呼びます)(上の例で言うと
commonFunctons
オブジェクト)を参照しているはずです。
それはChromeなどのブラウザであれば、date1.__proto__ date2.__proto__ date3.__proto__で確認できます。
これは上一部しか表示してませんが、Dateオブジェクトでよく使う関数がオブジェクトの中に入っているのがわかりますよね。上の例で言う
commonFunctions
そのものの参照が取得出来ているのがわかると思います。さらに、上の例では、全てのオブジェクトに、同じ
commonFunctions
オブジェクトの参照を入れてましたよね?
なので、date1.__proto__ === date2.__proto__ // true date2.__proto__ === date3.__proto__ // true date1.__proto__ === date3.__proto__ // trueこうなります。つまり、どのDateオブジェクトも、全て全く同じプロトタイプへの参照(厳密等価演算子
===
による比較で判断できる)を保持しています。プロトタイプ本体はどこに?
では、Dateオブジェクトがみんな参照している一つのプロトタイプは、実際どこにあるのでしょうか。
それは、Date
関数=Date
コンストラクタが所持しています。コンストラクタが所持しているプロトタイプは、
.prototype
でアクセス出来ます。
Date.prototype
によって得られたオブジェクトは、date1.__proto__
で得られたオブジェクトと全く同じです。// 厳密等価演算子でtrueが返る Date.prototype === date1.__proto__ // trueつまりjsでは、オブジェクトの共通の挙動として、自身のコンストラクタのプロトタイプを参照します。
プロトタイプチェーン
さて、プロトタイプは発見できたので、プロトタイプ上にある関数を実行してみましょう。
date1.getFullYear(); // 20192019が返ってきました。
でもこれ、よく考えると変ですよね?
getFullYear
を所持しているのはdate1
ではなく、Dateオブジェクトのプロトタイプ(date1.__proto__
であり、Date.prototype
)のはずです。つまり、
date1.__proto__.getFullYear();って呼び出すべきじゃない?
そこで登場するのがjsのプロトタイプチェーンという仕組みです。
jsでは、とあるオブジェクトに対して存在しないプロパティが参照された場合、そのオブジェクトのプロトタイプに同名のプロパティがあるかどうか探しに行きます。
そして見つかれば、そのプロパティを返します。つまり
date1
で言えば、date1.getFullYear
が呼び出された時点で、まずdate1
オブジェクトの中にgetFullYear
プロパティが存在するかどうかをチェックします。もちろん、代入していないのでありません。
次に、date1
の__proto__
の中を探します。ここにはありました!ということで、Date.prototype.getFullYear
を返します。この文字面、どこかで見覚えありませんか?
そう、MDNで調べるとよく出てくる記述です。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/getFullYear
これこれ!
じゃあ、逆に言えば、
date1
オブジェクトにgetFullYear
プロパティがあったら、そっち返すっていうことだよね?const date1 = new Date(); date1.getFullYear = 123; // 問題なく通る結果は...
date1.getFullYear(); // 123123が返ってきました。
先ほど書いたプロトタイプチェーンの仕様通りですね。しかし、ここまでは「チェーン」って言うほどチェーンしてません。
ということでここから、継承の話に移ります。継承
jsで言う継承とは、プロトタイプの繋がりを指します。
プロトタイプチェーンは、そのオブジェクトのプロトタイプに該当するプロパティが存在しなかった場合、プロトタイプのプロトタイプに探しに行きます。
そしてそこにも無い場合はプロトタイプのプロトタイプのプロトタイプに...というように、プロトタイプをどんどん遡って行き、最終的にプロトタイプがnullを返すまでこのチェーンは続きます。例えば、date1のプロトタイプをたどると、以下のようになります。
date1.__proto__ === Date.prototype // true date1.__proto__.__proto__ === Object.prototype // true date1.__proto__.__proto__.__proto__ === null // trueつまりここでは、
Object > Date > date1
という継承関係になっていると言えます。クラスベースで言うなら、
class Object { } class Date extends Object { } Date date1 = new Date();という感じですね。
複数回のプロトタイプチェーン
先ほどの
date1
オブジェクトからは、hasOwnProperty
関数が実行できます。date1.hoge = "HOGE"; date1.hasOwnProperty("hoge"); // trueこの関数は「継承由来ではない特定のプロパティを持っているかどうか調べる関数」ですが、下記のように、
Date.prototype
にはhasOwnProperty
関数は存在していません。// `Date.prototype`に、`"hasOwnProperty"`という独自のプロパティが存在するかどうかを確認し、`false`が返っています(つまり持っていない)。 Date.prototype.hasOwnProperty("hasOwnProperty") // falseではこの
hasOwnProperty
は誰が持っているかというと、Object.prototype.hasOwnProperty
が持っています。Object.prototype.hasOwnProperty("hasOwnProperty") // trueつまり、
date1
に対してhasOwnProperty
プロパティが呼び出された場合、Date.prototype
を探して発見できず、さらにそのプロトタイプ(Object.prototype
)を探してそこで発見し、Object.prototype.hasOwnProperty
を返します。プロトタイプとして
null
が返ってくるまで見つからなかった場合、jsは返り値としてundefined
を返します。プロトタイプチェーンの終端の実験
プロトタイプチェーンの終端が
null
だということは、自分でnull
を代入したらプロトタイプチェーンは止まるんでしょうか、やってみましょう。date1.__proto__.__proto__ = nullこれで、本来であれば
Object.prototype
を参照しているはずのdate1.__proto__.__proto__
がnull
になります。この状態で、Object.prototype
に存在するプロパティを呼び出すと...date1.hasOwnProperty("hasOwnProperty")Uncaught TypeError: date1.hasOwnProperty is not a function at <anonymous>:1:7関数じゃないって?
じゃあ...date1.hasOwnProperty // undefined
undefined
でした。というように、自身に存在していないプロパティでも、プロトタイプを再帰的に遡って該当するプロパティを引っ張ってくる仕組みが、プロトタイプチェーンです。
そして、このプロトタイプチェーンで繋がる関係を継承と呼びます。
コンストラクタ
さて、先ほどから普通に使用していますが、コンストラクタとはなんでしょうか。
jsにはクラスは存在しませんが、コンストラクタは存在します。jsにはオブジェクトを作る方法が2種類存在し、それが純粋なオブジェクト生成(オブジェクトリテラルや、
new Object()
など)と、new
演算子を使用したオブジェクト生成です。後者の
new
を使う方法は、関数と共に記述します。JSにおいて、このnew
と共に使用される関数のことを、コンストラクタと呼びます。// `Date`がコンストラクタ const date = new Date();コンストラクタとなる関数の条件は、アロー関数ではないこと、のみです。
つまり、先ほどの
doSomething
という関数もnew
をつければコンストラクタになります。const instance = new doSomething(); // 問題なく通るコンストラクタ関数はアッパーキャメルケース(UpperCamelCase)で書く慣習がありますが、上記のように文法レベルでの制約はありません。
クラスベースのオブジェクト指向に慣れていると
new Date()
を見たときに「Dateクラスのインスタンスを作っているな」と見えるのですが、これはプロトタイプベースな目線で見ると、「Date
関数をコンストラクタとしてインスタンスオブジェクトを作っている」と言えるでしょう。
(ただ、ES6でclass構文が出来てからは、ここで言うコンストラクタをクラスと呼ぶようにもなりました。ので、実際のところ「Dateクラスのインスタンス」と言うのも正しいと思います。実際自分で書く際はclass構文の方が楽だし読みやすい...ただシンタックスシュガーであることに変わりはないので、実際に行われているのはプロトタイプを用いたオブジェクト生成です)
new
のお仕事では、通常のオブジェクト作成と
new
によるオブジェクト作成は何が違うんでしょうか。
- 空の JavaScript オブジェクトを生成する
- このオブジェクト (のコンストラクター) を他のオブジェクトへリンクする
- ステップ 1 で新しく生成されたオブジェクトを this コンテキストとして渡す
- 関数が自分自身を返さない場合は this を返す
注目すべきは
- このオブジェクト (のコンストラクター) を他のオブジェクトへリンクする
です。
ここだけ曖昧な表現になっていてよくわかりませんが、具体的に意識するべきなのは以下の2点です。
- 新規作成したオブジェクトの
constructor
プロパティにコンストラクタ関数自体の参照を代入する。- 新規作成したオブジェクトの
[[prototype]]
にコンストラクタ関数のプロトタイプの参照を代入する。jsのES5までの仕様では、インスタンスオブジェクトのプロトタイプへの直接の書き込み権限がプログラマにありません(
[[prototype]]
がプライベートであり、かつ__proto__
は仕様に存在しなかったため)。つまり、
new
を使ってのみ、プロトタイプ継承を用いたオブジェクト生成をすることができます。jsにおける
new
とは、継承されたオブジェクトを作成するベストな方法であると言えるでしょう。class構文とプロトタイプ継承
TODO: 同じものを書く場合の比較とかを頑張って書く
オートボクシング
TODO: プリミティブ値とプロトタイプの話を頑張って書く
プロトタイプ汚染
TODO: 頑張って書く
おしまい
ということで、現状まだ書いてないことがいっぱいあるのですが、プロトタイプの最低限を書いたので一旦終わりとします。
続きはまたどこかで頑張って書きます。
- 投稿日:2019-12-24T23:18:42+09:00
Javascriptレシピ⑦
Javascriptの簡単なメモです。
アプリ制作に応用できる基本レシピですので、参考にされたい方はどうぞ。window.closeメソッド
window.closeメソッドはブラウザのタブを閉じる処理を行います。
ブラウザのタブはどれもwindowオブジェクトが持ってるのでwindow.closeメソッドでタブの操作が出来ます。
let _window; _window = window.open('http://ttt'); setTimeout(() => _window.close(), 3000);includesメソッド
文字列や配列の中に引数で指定した値が含まれているかどうかをチェックしてくれます。
第二引数に開始インデックスを数値として指定できます。const arr = 'wanko pome'; arr.includes('o', 3); //trueindexOfメソッドも似たような役割ですが、こちらは指定した値が含まれる場合は要素のインデックスを、ない場合は−1を返します。
指定した値が含まれているかどうかだけのチェックではなく、その値に対して何か操作を加えたいケースではindexOfを使います。const wanko = 'pome'; wanko.indexOf('p'); //0大文字小文字関係なく調べたい場合はtoLowerCaseやtoUpperCase
wanko.toUpperCase().includes('w');正規表現で文字列の抽出
正規表現を使うと複雑なパターンによる文字列検索が行なえます。
URLやメールアドレスの形式になっているか、スクレイピングの判断に役立ちます。書き方は二種類あります。
①スラッシュで囲まれた正規表現リテラルを使用する
定数として決められたパターンを照合させたい時に使います。
const wanko =/abc/;②正規表現オブジェクトのRegExpを使用する
動的な操作に対し使用します。
const wanko = new RegExp('abc');文字列に一致するものがあるかテストするメソッドをRegExpは持っています。
const str = '32312edwd1d1'; /ab*c+[0-9]*/g.test(str);アクセス元の端末がスマホ化どうかを判定する
端末情報はJavasriptのnavigatorオブジェクトに格納されています。
navigatorオブジェクトのuserAgentプロパティによってスマホかどうか判断します。
userAgentプロパティを文字列検索かけてスマホ化タブレットか判定していきます。const ut = navigator.userAgent; if(ut.indexOf('iPhone') > 0 || ut.indexOf('iPod') > 0){ 'smaho'; } else if(ut.indexOf('iPad'){ 'tablet';}else{ 'pc'; }テキストボックスから値を取得
テキストボックスの要素をJavascriptで指定し、その要素のvakueプロパティを参照することで取得できます。
①getEementByIdを使う方法
const message = document.getElementById('input').value;②フォームからたどっていく方法
const message = document.forms.form1.input.value;ミリ秒単位の経過時間を取得する方法
ミリ秒は1000ミリ秒で1秒に換算されます。
onClkickでstart()とstop()をボタン要素に書きます。
変数宣言をわすないで下さい。function start(){
stare_time = new Date();
}function stop(){
end_time = new Date();
diff = end_time.getTime() - start_time.getTime();
}マウスオーバーイベント
マウスオーバーイベントは要素にマウスが乗った時に発火するイベントです。
逆にマウスが離れた時に発火するmouseoutイベントもあります。wanko.addEventListner('mouseover', function(event){ // }, false);//forで複数の要素に対してマウスオーバーを指定しています。 for(i = 1; i <= 3; i++) { const elem = document.getElementById('div' +i); elem.addEventListener('mouseover', event => { event.target.style.baclground = 'orange'; setTimeout(function() { evet.target.style.backgroundColor = ''; },500); }, false); }
- 投稿日:2019-12-24T23:04:23+09:00
誰でも小説家になれるWebアプリ
IT芸人 Advent Calendar 2019 25日目の記事です。
前置き
皆様おはようございます。DE-TEIUです。
今回はIT芸人の記事ということで、ちょっとしたWebアプリを開発しました。
こいつどの記事でも結局やってる事同じとか言うな成果物
「無限の猿定理」という言葉をご存知でしょうか。
これは、「文字列をランダムに並べ続けると、どんな文字列でもいつかは見つかるはずである」という定理の事です。
猿が適当にキーボードをガチャガチャやっていればいつかは文学作品を書き上げる、というような表現が名前の由来になっているようです。とはいえ、時間は有限なので、文学作品を書き上げたいからといって本当に完成するまで適当にキーボードを叩くわけにもいきません。
と、いうわけで、誰でも手っ取り早く文学作品を書き上げられるWebアプリを開発しました。
文学史に残る名著が
書けたと思います。
これを使えば自分でキーボードを叩くか、とかげやねこがキーボードの上を歩くだけでも作品が出来上がります。
書いている最中はきっと無我夢中で書いていて内容が頭に入ってこないと思われますので、書き上げてからじっくり読みましょう。解説
概要
このアプリは、静的サイトを公開するWebサーバーと、青空文庫の著作権切れの作品をランダムに取得するためのAPIサーバーの2つで構成されています。
- 静的サイトをnetlifyで公開
- 文学作品情報を取得するためのAPIをNode.jsで実装し、ZEIT Nowで公開
青空文庫について
青空文庫とは、著作権切れ(あるいは公開が許可されている)文学作品が読めるWebサイトです。
今回は、公開中 作家リスト 全てから作品一覧のCSVファイルをダウンロードし、そこから著作権切れの作品の公開URLを抽出し、APIに組み込みました。解説
以下、実装の解説等です。
青空文庫の作品詳細ページから作品情報を取得
青空文庫の作品の詳細ページ(例えばこちらなど)から、htmlをダウンロードし、そこから作品名、作者名、本文、訳者を抽出します。
この詳細ページは、文字コードがShift-JISになっているため、ダウンロード時にUTF-8にエンコードします。main.js(抜粋)//文字コード変換にはIconvを使用 import { Iconv } from 'iconv'; const sjis2utf8 = new Iconv('SHIFT_JIS', 'UTF-8//TRANSLIT//IGNORE'); //htmlファイルダウンロードはaxiosで行う const axios = require("axios"); //ダウンロードしたHTMLをDOMとして加工するため、JSDOMを使用 const jsdom = require("jsdom"); const { JSDOM } = jsdom; app.get('/getdata', (req, res) => { const url = "作品URL"; axios.get(url, { responseType: 'arraybuffer', //axios実行時にtransformResponseを仕込んでおくと、レスポンスデータを任意の処理で加工して取得できる transformResponse: [(data) => { //レスポンスデータの文字コードをShift-JISからUTF-8に変換 return sjis2utf8.convert(data).toString(); }], }).then(apires => { const text = apires.data; const dom = new JSDOM(text); const document = dom.window.document; //図書目録の表示を削除 const bibliographicalInformation = document.querySelector(".bibliographical_information"); bibliographicalInformation.parentNode.removeChild(bibliographicalInformation); //本文を抽出する //全角文字を除外 //先頭の改行コードを除外 const mainText = format(getTextContent(document, ".main_text")); //タイトルを抽出 const title = getTextContent(document, ".title"); //作者を抽出 const author = getTextContent(document, ".author"); //訳者を抽出 const translator = getTextContent(document, ".translator"); const result = { url, title, author, translator, mainText }; res.status(200); res.send(result); }).catch(err => { //色々エラー時の処理 res.status(500); ... ... res.send({}); }); });/** * DOM要素を検索し、その中の文字列を抽出 * @param {*} document DOM * @param {*} condition 検索条件 */ const getTextContent = (document, condition) => { const result = document.querySelector(condition); if (!result) { return ""; } return result.textContent; }; /** * 文書のフォーマット(空白のトリミング) * @param {*} text テキスト */ const format = (text) => { if (!text) { return; } const result = text.replace(/ /g, ''); return result.trim(); }Svelteでフロントエンド実装
今回、フロントエンドのロジックの実装にはSvelteを採用しました。(理由はただ単に使ってみたかっただけです)
Svelteとは、シンプルなJavascriptコンパイラです(フレームワークではありません)。使い方としては、
コンポーネント(~~~.svelte)の中にhtml,css,javascriptを実装し、コンパイルするだけです。このあたりはVueなどの昨今のJSフレームワークに似ています。
しかしながら、Svelteでコンパイルを実行すると、結果はただのhtml,css,javascriptファイルとして出力されます。Svelte固有のロジックなどはほぼ残らないようです。所感ですが、Vueを更に簡略化したような書き方ができるので、Vueユーザーであればわりとすんなり馴染めるのではないでしょうか。
参考:ReactとVueを改善したSvelteというライブラリーについて
Svelte導入方法
公式ページからの引用です。
$ npx degit sveltejs/template my-svelte-project $ cd my-svelte-project $ npm install $ npm run devこれだけで、プロジェクトの作成とデバッグ実行までが可能です。
その他、実装方法は公式サイトのTutorialとかExampleを参照してください。
(かなりちゃんとまとまっているので、この記事では細かい文法は省略します。)まとめ
Svelteは、書き方がシンプルなので今回のような小規模なWebアプリの開発に向いてそうですね。
- 投稿日:2019-12-24T22:09:44+09:00
webサイトの「URLを共有する」を実現するために使ったJavaScriptライブラリをまとめた
前提(本質的には関係ないです)
- Spring Bootを利用したwebアプリケーションです。
- テンプレートエンジンはThymeleafを使用しています。
- HTMLは必要な部分のみ記載しています。
- jQueryを使用しています。
QRコード作成
qrcode-generator
https://cdnjs.com/libraries/qrcode-generator
https://www.npmjs.com/package/qrcode-generator具体例
send_by_qr.html<!-- ~~ --> <div class="form-group offset-sm-3 col-sm-3" id="qrcode"> ここにQRコードが入ります </div> <!-- サーバサイドから渡されたURLをいったん保持します --> <input type="hidden" th:value="${qr}" id="qrvalue" /> <!-- ~~ --> <script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/qrcodegenerator/x.x.x/qrcode.min.js"></script>make_qr.js// 画面表示時にid指定したDOM要素のvalueからQRコードを生成します。 $(function() { var qr = qrcode(0, 'M'); if ($('#qrcode').length > 0) { var qr = qrcode(0, 'M'); qr.addData($('#qrvalue').val()); qr.make(); $('#qrcode').html(qr.createImgTag(4, 16)); } });LINEで共有
具体例
send_by_line.html<!-- ~~ --> <a id="icon_line" class="任意のCSS"><i class="任意のCSS"></i>LINEで共有する</a> <!-- ~~ -->send_by_line.js$(function() { $('#icon_line').on('click', function(){ window.open("https://social-plugins.line.me/lineit/share?url=" + encodeURIComponent($('#qrvalue').val()), "_blank", "top=50,left=50,width=500,height=500,scrollbars=1,location=0,menubar=0,toolbar=0,status=1,directories=0,resizable=1"); return false; }); });クリップボードにコピー
clipboardjs
https://clipboardjs.com/
https://haniwaman.com/clipboard-js/#clipboardjs具体例
send_by_clipboard.html<!-- ~~ --> <p class="form_label mb32"> <textarea id="text_area_qr" th:text="${qr}" class="form_control">https://welnomi.jp</textarea> </p> <p> <button class="任意のCSS" data-clipboard-target="#text_area_qr">クリップボードにコピー</button> </p> <input type="hidden" th:value="${qr}" id="qrvalue" /> <!-- ~~ --> <!-- clipboardjsはライブラリをダウンロードしてよしなに配置しています。 --> <script th:src="@{/js/clipboard.min.js}"></script>send_by_clipboard.js$(function() { new ClipboardJS('.btn-clipboard').on('success', function(e) { // ぬるっと現れてふわっと消える $('.clipboard-success').fadeIn(100).fadeOut(2000); }); });
- 投稿日:2019-12-24T21:41:03+09:00
Moment.jsを使って年齢計算
サンプル
sample.jsimport moment from "moment"; function calcAge() { dateOfBirth = moment(new Date("1994-01-01")); // 生年月日 today = moment(new Date("2019-12-24")); // 今日の日付 // 西暦を比較して年齢を算出 let baseAge = today.year() - dateOfBirth.year(); // >> 2019 - 1994 = 25(歳) // 誕生日を作成 >> 2019-01-01 let birthday = moment( new Date( today.year() + "-" + (dateOfBirth.month() + 1) + "-" + dateOfBirth.date() ) ); console.log(birthday) // >> 2019-01-01 // 誕生日を過ぎている または 今日が誕生日である場合は、算出した年齢をそのまま返す if (today.isAfter(birthday) || today.isSame(birthday, "day")) { return baseAge; // 25(歳)が返却される } // 誕生日を過ぎていない場合は-1した年齢を返す return baseAge - 1; // 24(歳)が返却される }
moment1.isAfter(moment2)・・・moment1がmoment2より未来の日付かどうか
moment1.isSame(moment2, "day")・・・moment1とmoment2が同じ日付かどうか。
→"month"を指定した場合は、年月が同じかどうか
→ "year"を指定した場合は、西暦が同じかどうか
正しい日付かどうか
sample.jsimport moment from "moment"; /** * 年齢計算 * * @param {Date} baseday * @param {Date} birthday */ function calcAge(baseday, birthday) { console.log(moment(baseday).isValid()); // 正しい日付ならtrue, 不正な日付はfalse console.log(moment(birthday).isValid()); }
- 投稿日:2019-12-24T21:11:24+09:00
Rails6でjqueryアニメーションライブラリanimsitionの使用 | 躓いたことなど...
1. rails6でanimsitionを使う
ビューにアニメーションを付けようと思い、試しにanimsitionというjqueryライブラリを使った.いくつか、躓いて勉強になったことをまとめる.
2. まずはダウンロード
ライブラリを下記のサイトからダウンロード(ZIP形式)
https://github.com/blivesta/animsition3. webpacker経由だと動かない...
★解凍後、以下の2ファイルををapp/javascript/srcにコピー.
①dist/js/animsition.js
②dist/js/animsition.css
★app/javascript/packs/application.jsに追記application.jsimport "../src/animsition.js"; import "../src/animsition.css";★app/views/layouts/application.html.erbのheadタグ内に追記
➡jqueryを読み込む
➡application.jsを読み込む
これでビューでanimsition.jsとanimsition.cssを読み込めたはず...
readmeでanimsition.jsより先にjqueryを読み込むと書いてあったので、順番もいいはず...application.html.erb<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>★ビューで動作確認のため、以下追記
show.html.erb<div class="animsition"> <a href="https://www.google.com/" class="animsition-link">animsition link 1</a> <a href="https://www.google.com/" class="animsition-link">animsition link 2</a> </div> <script> $(document).ready(function() { $(".animsition").animsition({ inClass: 'rotate-in', outClass: 'rotate-out', inDuration: 1500, outDuration: 800, linkElement: '.animsition-link', // e.g. linkElement: 'a:not([target="_blank"]):not([href^="#"])' loading: true, loadingParentElement: 'body', //animsition wrapper element loadingClass: 'animsition-loading', loadingInner: '', // e.g '<img src="loading.svg" />' timeout: false, timeoutCountdown: 5000, onLoadEvent: true, browser: [ 'animation-duration', '-webkit-animation-duration'], // "browser" option allows you to disable the "animsition" in case the css property in the array is not supported by your browser. // The default setting is to disable the "animsition" in a browser that does not support "animation-duration". overlay : false, overlayClass : 'animsition-overlay-slide', overlayParentElement : 'body', transition: function(url){ window.location.href = url; } }); }); </script>ページをリロードして確認...動かず...エラー出てる
animsition is not a functionみたいなよくあるエラー
animsition .jsファイルが読み込めてるか確認のため、jsファイルにalert文を仕込んだが、問題なく実行されていた.animsition .cssも一応確認したが、問題なし.4. assets内にライブラリを配置して、読み込むと動いた!
webpacker経由で読み込むのはやめて、assets内からライブラリを読み込むようにしてみた.
★app/assets/の中にanimsition.jsとanimsition.cssをコピー
★application.html.erbに下記を追記
javascript_include_tag で個別に読み込んでみた.application.html.erb<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script> <%= javascript_include_tag 'animsition.js' %>★app/config/initializers/assets.rbに下記を追記
これを書かないとjavascript_include_tag が動かないらしい...assets.rbRails.application.config.assets.precompile += %w( animsition.js )★ページをリロードして確認...動いた!
5. redirect_to以外の方法で遷移したページのアニメーションが動かない...
アニメーションが無事動いたが、問題が発覚.
redirect_toで遷移するページに記載したアニメーションは動作するが、redirect_toを使用しないで、遷移したページに記載したアニメーションは動作しないことがわかった.6. data-turbolinkのせい!
どうやら、data-turbolinkの仕業でした.
data-turbolinkはheadタグを最初の1回しか読まずに、ページの表示を高速化するgemで、rails4からデフォルトで導入されている模様...
そして、data-turbolinkが働いて遷移したページではreadyイベントが発火しない事があるらしい.★data-turbolinkをオフにする
遷移元のリンクタグにdata属性を追加<%= link_to "マイページ", current_user, data: {"turbolinks" => false} %>この設定を付けて、ページ遷移すれば、遷移先のアニメーションは無事動作した.
参考にしたページ
★Rails 4のturbolinksについて最低でも知っておきたい事
https://kray.jp/blog/must-know-about-turbolinks/
★Rails 5.0でlink_toでturbolinkを無効にする法
https://qiita.com/hodosan/items/ee84482d18d6dccd9488
- 投稿日:2019-12-24T20:33:54+09:00
これからはFunction Componentですべて解決できる――というのはどうやら幻想だったようです。
何がしたかったのか
Reactには、Lazy Componentというものがあります。
MyComponent.tsximport React, { FC } from 'react'; const MyComponent: FC = () => ( <div>Hello LazyComponent!</div> ); export default MyComponent;MyApp.tsximport React, { FC, Suspense, lazy } from 'react'; const MyComponent = lazy(() => import('./MyComponent')); const MyApp: FC = () => ( <div> <Suspense fallback={<div>Loading...</div>}> <MyComponent /> </Suspense> </div> ); export default MyApp;とすると、MyComponentのロードが完了するまでfallbackに設定された
<div>Loading...</div>
を代わりにレンダリングしてくれるというものです。で、いろいろ調べてたらこんなこともできると判明。
LazyComponent.jsimport React, { Component } from 'react'; let result = null; const timeout = (msec) => new Promise(resolve => { setTimeout(resolve, msec) }); const LazyComponent = () => { if (result !== null) { return ( <div>{result}</div> ) } throw new Promise(async(resolve) => { await timeout(1000); result = 'Done' resolve(); }) }; export default LazyComponent;こう書いたら、throwしたPromiseがresolveされたときにもう1回レンダリングされるらしく。私の探し方が悪いのか何なのか、この仕様はReactのドキュメント上で見つけることができませんでした。どこに書いてあるのか知っている人がいたらこっそり教えてほしいです。
それはさておきこの仕様、ドキュメントで見つからなかったので動かない前提で試しに書いてみました。
試しに書いたコードimport React, { FC, lazy, Suspense } from 'react'; const PromiseTest= lazy(async () => { let state = 0; const TestInner: FC = () => { if(state) { return ( <div>Done! {state}</div> ) } throw new Promise((res) => { setTimeout(() => { state = 5; res(); }, 5000); }); }; return { default: TestInner, }; }); const TestApp: FC = () => { return ( <div> <Suspense fallback={<div>WAITING...</div>}> <PromiseTest /> </Suspense> </div> ); }やってみた結果……動く!動くぞ!
さて、問題のコードに移ろうじゃないか
さて、Promiseをthrowしたら期待通りに動くことが分かったんですけれど。
state = 5
ってPromiseの中で変数に代入しちゃってるじゃないですか。ぶっちゃけキモいですよね。
useState
フックに置き換えてもいけるんじゃね?って思った私、置き換えてみました。置き換えてみたimport React, { FC, lazy, Suspense, useState } from 'react'; const PromiseTest= lazy(async () => { const TestInner: FC = () => { const [state, setter] = useState(0); if(state) { return ( <div>Done! {state}</div> ) } throw new Promise((res) => { setTimeout(() => { setter(5); res(); }, 5000); }); }; return { default: TestInner, }; }); const TestApp: FC = () => { return ( <div> <Suspense fallback={<div>WAITING...</div>}> <PromiseTest /> </Suspense> </div> ); }あれ、動かん
動かんぞ。
useState
に置き換える前は動いたコードが、置き換えた瞬間動かなくなりました。てゆうか、setter
は普通に呼ばれているはずなのに、state
の値は0のまま。なんでや、、、。諦めてComponent classにしてみた
というわけで、
PromiseTest
の実装をComponent classに変えてみました。state
をthis.state.state
、setter
をthis.setState
に変えただけですけどね。classに書き換えてみたclass TestInner extends React.Component<{}, { state: number }> { constructor(props: {}) { super(props); this.state = { state: 0, }; } render() { if(this.state.state) { return ( <li>Done! {this.state.state}</li> ); } throw new Promise((res) => { setTimeout(() => { console.log('resolved'); this.setState({ state: 5 }); res(); }, 5000); }); } } const PromiseTest = lazy(async () => { return { default: TestInner, }; }); const TestApp: FC = () => { return ( <div className='board-list-container'> <Suspense fallback={<div>WAITING...</div>}> <PromiseTest /> </Suspense> </div> ); }こうすると、動きました。動いてしまいました。え、なんでなんや、、、
……Function Componentが使えない極めてまれなケースの1つを発見した身としては、非常に頭が痛いです。こういう重要なことはもっとわかりやすくドキュメントに書いておいて下せぇ……。結論
React、なんもわからん。
- 投稿日:2019-12-24T20:04:26+09:00
Dispose three.js objects with Nuxt
Nuxt のページ遷移時に three で作ったオブジェクトを破棄したいとき、メモリリークをケアするための tips です。
少し前にちょうどアドベカレンダーで次の three x Nuxt な記事が書かれていました。
この内容で Nuxt で three をやっていくのはつかめると思うので、破棄部分のみ書いていきます。先述の記事には EventBus で Vue コンポーネント側から three の描画スクリプトのメソッドを発火させていましたが、Vue から剥がしやすくしておくために、今回は data にインスタンスを格納して取り回していくことを想定しています。
パフォーマンス考えるならやはり Vue に依存しない自前の EventBus で実装するとか Three の
EventDispatcher
を使うのがよいかなと思います。もしくは Non-reactive data とか。(場合によりけりですね、Vue 縛りなら先述の記事のように対応するのが一番簡単でスマートかと … また追記します)Page/xxx.vue の
beforeDestroy
で破棄メソッドを実行します。このときcanvas が消えるので、UX を考えると、先に loader の展開を待ってから実行するなど考慮しておくとよいですね。pages/xxx.jsimport { NiceThreeScene } from '~/assets/js/NiceThreeScene' export default { data() { return { canvas: null, frameId: 0, } }, mounted() { this.init() }, beforeDestroy() { this.canvas.finish() cancelAnimationFrame(this.frameID) }, methods: { init() { const { canvas } = this.$refs this.canvas = new NiceThreeScene({ canvas }) // this.update() }, update() { this.frameId = requestAnimationFrame(this.update) this.canvas.update() }, }, }assets/NiceThreeScene.jsexport class NiceThreeScene { constructor({ canvas }) { // ... init ... } /** * called by raf */ update() { if (!this.needsStopUpdate) { const time = 0.001 * performance.now() this.animationObjects.update(time) // this.renderer.render(this.scene, this.camera) } } /** * called by beforeDestroy */ finish() { this.needsStopUpdate = true // stop update method // this.disposeThreeObjects(this.scene, this.renderer) // dispose // this.canvas.width = 1 // resize canvas this.canvas.height = 1 // resize canvas } disposeThreeObjects(scene, renderer) { scene.children.forEach(obj => { obj.traverse(obj3D => dispose(obj3D)) scene.remove(obj) }) renderer.dispose() renderer.forceContextLoss() renderer.domElement = null } } function dispose(obj) { if (obj.geometry) { obj.geometry.dispose() obj.geometry = null } if (!!obj.material && obj.material instanceof Array) { obj.material.forEach(material => disposeMaterial(material)) } else if (obj.material) { disposeMaterial(obj.material) } } function disposeMaterial(material) { if (material.map) { material.map.dispose() material.map = null } material.dispose() material = null }ざっくりまとめると update の処理をスキップ、 three のオブジェクトを破棄して canvas をリサイズする流れです。
こちら Three.js Advent Calendar 2019 12.24 でした。
おわります。
メリークリスマス??
- 投稿日:2019-12-24T19:55:45+09:00
Quill EditorでHTML5をそのまま挿入する方法
ブラウザ
説明
個人productにアフィリエイトのリンクを埋め込もうとしたときに、できへんやんってなったのでQuillでリンクのためのhtmlをそのままぶち込めるようにした。ついでに、マークダウンでも書けるようになった。
実装
<!DOCTYPE html> <html lang="ja"> <style> #editor-container { height: 200px; } #markdown { background-color: #eeffee; min-height: 200px; } #html { background-color: #ffeeee; min-height: 200px; } #output-quill { background-color: #ffeeff; min-height: 200px; ol .ql-indent-1 { margin-left: 200px; } } #output-markdown { background-color: #ffeeff; min-height: 200px; } </style> <head> <!-- Include stylesheet --> <link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet"> </head> <body> <h1>Quill JS Editor</h1> <hr> <div id="editor-container"> </div> <h1>Rendered Markdown</h1> <hr> <div id="output-markdown"></div> <script src="https://cdn.quilljs.com/1.3.6/quill.js"></script> <script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/to-markdown/3.0.4/to-markdown.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/8.3.1/markdown-it.min.js"></script> <script> (function() { function init(raw_markdown) { var quill = new Quill("#editor-container", { modules: { toolbar: [ [{ header: [1, 2, false] }], ["bold", "italic", "underline"], [{ 'list' : 'ordered' }, { 'list' : 'bullet' }], ["image", "code-block"] ] }, placeholder: "Compose an epic...", theme: "snow" // or 'bubble' }); var md = window.markdownit(); md.set({ html: true }); var result = md.render(raw_markdown); quill.clipboard.dangerouslyPasteHTML(result + "\n"); // Need to do a first pass when we're passing in initial data. var html = quill.container.firstChild.innerHTML; $("#markdown").text(toMarkdown(html)); $("#html").text(html); $("#output-quill").html(html); $("#output-markdown").html(result); // text-change might not be the right event hook. Works for now though. quill.on("text-change", function(delta, source) { var html = quill.container.firstChild.innerHTML; var markdown = toMarkdown(html); var rendered_markdown = md.render(markdown); $("#markdown").text(markdown); $("#html").text(html); $("#output-quill").html(html); $("#output-markdown").html(rendered_markdown); }); } // Just some fake markdown that would come from the server. var text = ""; text += "# Dillinger" + "\n"; text += " " + "\n"; text += "[](https://nodesource.com/products/nsolid)" + "\n"; text += " " + "\n"; text += "Dillinger is a cloud-enabled, mobile-ready, offline-storage, AngularJS powered HTML5 Markdown editor." + "\n"; text += " " + "\n"; text += " - Type some Markdown on the left" + "\n"; text += " - See HTML in the right" + "\n"; text += " - Magic" + "\n"; text += " " + "\n"; text += "# New Features!" + "\n"; text += " " + "\n"; text += " - Import a HTML file and watch it magically convert to Markdown" + "\n"; text += " - Drag and drop images (requires your Dropbox account be linked)" + "\n"; text += " " + "\n"; text += " " + "\n"; text += "You can also:" + "\n"; text += " - Import and save files from GitHub, Dropbox, Google Drive and One Drive" + "\n"; text += " - Drag and drop markdown and HTML files into Dillinger" + "\n"; text += " - Export documents as Markdown, HTML and PDF" + "\n"; text += " " + "\n"; text += "Markdown is a lightweight markup language based on the formatting conventions that people naturally use in email. As [John Gruber] writes on the [Markdown site][df1]" + "\n"; text += " " + "\n"; text += "> The overriding design goal for Markdown's" + "\n"; text += "> formatting syntax is to make it as readable" + "\n"; text += "> as possible. The idea is that a" + "\n"; text += "> Markdown-formatted document should be" + "\n"; text += "> publishable as-is, as plain text, without" + "\n"; text += "> looking like it's been marked up with tags" + "\n"; text += "> or formatting instructions." + "\n"; text += " " + "\n"; text += "This text you see here is *actually* written in Markdown! To get a feel for Markdown's syntax, type some text into the left window and watch the results in the right." + "\n"; text += " " + "\n"; text += "### Tech" + "\n"; text += " " + "\n"; text += "Dillinger uses a number of open source projects to work properly:" + "\n"; text += " " + "\n"; text += "* [AngularJS] - HTML enhanced for web apps!" + "\n"; text += "* [Ace Editor] - awesome web-based text editor" + "\n"; text += "* [markdown-it] - Markdown parser done right. Fast and easy to extend." + "\n"; text += "* [Twitter Bootstrap] - great UI boilerplate for modern web apps" + "\n"; text += "* [node.js] - evented I/O for the backend" + "\n"; text += "* [Express] - fast node.js network app framework [@tjholowaychuk]" + "\n"; text += "* [Gulp] - the streaming build system" + "\n"; text += "* [Breakdance](http://breakdance.io) - HTML to Markdown converter" + "\n"; text += "* [jQuery] - duh" + "\n"; text += " " + "\n"; text += "And of course Dillinger itself is open source with a [public repository][dill]" + "\n"; text += " on GitHub." + "\n"; text += " " + "\n"; text += "### Installation" + "\n"; text += " " + "\n"; text += "Dillinger requires [Node.js](https://nodejs.org/) v4+ to run." + "\n"; text += " " + "\n"; text += "Install the dependencies and devDependencies and start the server." + "\n"; text += " " + "\n"; text += "```sh" + "\n"; text += "$ cd dillinger" + "\n"; text += "$ npm install -d" + "\n"; text += "$ node app" + "\n"; text += "```" + "\n"; text += " " + "\n"; text += "For production environments..." + "\n"; text += " " + "\n"; text += "```sh" + "\n"; text += "$ npm install --production" + "\n"; text += "$ npm run predeploy" + "\n"; text += "$ NODE_ENV=production node app" + "\n"; text += "```" + "\n"; text += " " + "\n"; text += "### Plugins" + "\n"; text += " " + "\n"; text += "Dillinger is currently extended with the following plugins. Instructions on how to use them in your own application are linked below." + "\n"; text += " " + "\n"; text += "| Plugin | README |" + "\n"; text += "| ------ | ------ |" + "\n"; text += "| Dropbox | [plugins/dropbox/README.md] [PlDb] |" + "\n"; text += "| Github | [plugins/github/README.md] [PlGh] |" + "\n"; text += "| Google Drive | [plugins/googledrive/README.md] [PlGd] |" + "\n"; text += "| OneDrive | [plugins/onedrive/README.md] [PlOd] |" + "\n"; text += "| Medium | [plugins/medium/README.md] [PlMe] |" + "\n"; text += "| Google Analytics | [plugins/googleanalytics/README.md] [PlGa] |" + "\n"; text += " " + "\n"; text += " " + "\n"; text += "### Development" + "\n"; text += " " + "\n"; text += "Want to contribute? Great!" + "\n"; text += " " + "\n"; text += "Dillinger uses Gulp + Webpack for fast developing." + "\n"; text += "Make a change in your file and instantanously see your updates!" + "\n"; text += " " + "\n"; text += "Open your favorite Terminal and run these commands." + "\n"; text += " " + "\n"; text += "First Tab:" + "\n"; text += "```sh" + "\n"; text += "$ node app" + "\n"; text += "```" + "\n"; text += " " + "\n"; text += "Second Tab:" + "\n"; text += "```sh" + "\n"; text += "$ gulp watch" + "\n"; text += "```" + "\n"; text += " " + "\n"; text += "(optional) Third:" + "\n"; text += "```sh" + "\n"; text += "$ karma test" + "\n"; text += "```" + "\n"; text += "#### Building for source" + "\n"; text += "For production release:" + "\n"; text += "```sh" + "\n"; text += "$ gulp build --prod" + "\n"; text += "```" + "\n"; text += "Generating pre-built zip archives for distribution:" + "\n"; text += "```sh" + "\n"; text += "$ gulp build dist --prod" + "\n"; text += "```" + "\n"; text += "### Docker" + "\n"; text += "Dillinger is very easy to install and deploy in a Docker container." + "\n"; text += " " + "\n"; text += "By default, the Docker will expose port 80, so change this within the Dockerfile if necessary. When ready, simply use the Dockerfile to build the image." + "\n"; text += " " + "\n"; text += "```sh" + "\n"; text += "cd dillinger" + "\n"; text += "docker build -t joemccann/dillinger:${package.json.version}" + "\n"; text += "```" + "\n"; text += "This will create the dillinger image and pull in the necessary dependencies. Be sure to swap out `${package.json.version}` with the actual version of Dillinger." + "\n"; text += " " + "\n"; text += "Once done, run the Docker image and map the port to whatever you wish on your host. In this example, we simply map port 8000 of the host to port 80 of the Docker (or whatever port was exposed in the Dockerfile):" + "\n"; text += " " + "\n"; text += "```sh" + "\n"; text += "docker run -d -p 8000:8080 --restart=\"always\" <youruser>/dillinger:${package.json.version}" + "\n"; text += "```" + "\n"; text += " " + "\n"; text += "Verify the deployment by navigating to your server address in your preferred browser." + "\n"; text += " " + "\n"; text += "```sh" + "\n"; text += "127.0.0.1:8000" + "\n"; text += "```" + "\n"; text += " " + "\n"; text += "#### Kubernetes + Google Cloud" + "\n"; text += " " + "\n"; text += "See [KUBERNETES.md](https://github.com/joemccann/dillinger/blob/master/KUBERNETES.md)" + "\n"; text += " " + "\n"; text += " " + "\n"; text += "### Todos" + "\n"; text += " " + "\n"; text += " - Write MOAR Tests" + "\n"; text += " - Add Night Mode" + "\n"; text += " " + "\n"; text += "License" + "\n"; text += "----" + "\n"; text += " " + "\n"; text += "MIT" + "\n"; text += " " + "\n"; text += " " + "\n"; text += "**Free Software, Hell Yeah!**" + "\n"; text += " " + "\n"; text += "[//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax)" + "\n"; text += " " + "\n"; text += " " + "\n"; text += " [dill]: <https://github.com/joemccann/dillinger>" + "\n"; text += " [git-repo-url]: <https://github.com/joemccann/dillinger.git>" + "\n"; text += " [john gruber]: <http://daringfireball.net>" + "\n"; text += " [df1]: <http://daringfireball.net/projects/markdown/>" + "\n"; text += " [markdown-it]: <https://github.com/markdown-it/markdown-it>" + "\n"; text += " [Ace Editor]: <http://ace.ajax.org>" + "\n"; text += " [node.js]: <http://nodejs.org>" + "\n"; text += " [Twitter Bootstrap]: <https://twitter.github.com/bootstrap/>" + "\n"; text += " [jQuery]: <https://jquery.com>" + "\n"; text += " [@tjholowaychuk]: <https://twitter.com/tjholowaychuk>" + "\n"; text += " [express]: <http://expressjs.com>" + "\n"; text += " [AngularJS]: <https://angularjs.org>" + "\n"; text += " [Gulp]: <http://gulpjs.com>" + "\n"; text += " " + "\n"; text += " [PlDb]: <https://github.com/joemccann/dillinger/tree/master/plugins/dropbox/README.md>" + "\n"; text += " [PlGh]: <https://github.com/joemccann/dillinger/tree/master/plugins/github/README.md>" + "\n"; text += " [PlGd]: <https://github.com/joemccann/dillinger/tree/master/plugins/googledrive/README.md>" + "\n"; text += " [PlOd]: <https://github.com/joemccann/dillinger/tree/master/plugins/onedrive/README.md>" + "\n"; text += " [PlMe]: <https://github.com/joemccann/dillinger/tree/master/plugins/medium/README.md>" + "\n"; text += " [PlGa]: <https://github.com/RahulHP/dillinger/blob/master/plugins/googleanalytics/README.md>" + "\n"; text = "<ol><li>List Item 1<li><li><ol><li>Point a</li></ol></li></ol>"; init(text); })(); </script> </body> </html>
- 投稿日:2019-12-24T19:19:33+09:00
jQueryでスクロールダウンを導入してみた
ボタンを押した際に同じページ内で指定の場所にスクロールするやり方を実装してみました!
スクロールダウンの方法
今回はid名buttonを押した際にid名header部分にスクロールするように指定してみる
//変数を指定する var header = $('#header').offset().top; //id名buttonをクリックした際の関数を指定する $('#button').click(function(){ $('html,body').animate({ { scrollTop: header } )};解説
.offset()ってなんぞや?となると思うので、自分はなりました笑
.offsetとは指定した要素のドキュメントの左上からのy軸方向の距離を取得する。
つまり今回の例で言うとid名headerの位置をドキュメントから見てのy軸の距離を取得したという事になります。そしてその取得した値を変数headerに代入したと言う事になります。
- 投稿日:2019-12-24T17:54:47+09:00
MixPanelのデータをGASを使って取得する方法
MixPanelはサクサク動くし、分析もかゆいところに手が届くのでとても好きだが
他データソースのものと組み合わせて使おうとして躓いたのでメモMixPanelで取得したPVを他のデータソースと横断して分析したい
サービスを取り巻くデータは下記のように色々なものがある
- アプリケーションのDB
- SalesForce
- Firebase
- その他SendGridなどのSaaSなど
これとかを見る限りだと、基本的には全てBigQueryに突っ込んでそこから見るというのがベストプラクティス化しているように感じている
近年のデータ分析基盤構築における失敗はBigQueryを採用しなかったことに全て起因している
ということでMixPanelの情報もBigQueryに入れようとしたが、どうやら有料のサービスを使わないといけないらしい
https://mixpanel.com/data-pipeline/
Segment とかも注目していたので試してみたが、出力のみで、Mixpanelの情報を取得するということとかはできなさそう、定番のZapierも同じ
どうやらMixPanelは貯まったデータを取得する部分は弱いらしい
結局どうしたか
結局見たいデータがピンポイントだったのでGoogle App Scriptをスケジュール実行して
スプレッドシートに出力。そいつをredashで見に行くようにしたredashをspreadsheetの連携については下記が参考になる
Redash と Google Spreadsheet を連携するfunction fetchPV() { var today = new Date(); var fromDate = Utilities.formatDate(new Date(today.getFullYear(), today.getMonth()-2, 1), 'Asia/Tokyo', 'yyyy-MM-dd') var toDate = Utilities.formatDate(new Date(today.getFullYear(), today.getMonth()+1, 0), 'Asia/Tokyo', 'yyyy-MM-dd') var data = { 'params' : '{"from_date":"' + fromDate + '","to_date": "' + toDate + '"}', 'script' : 'function main(){return Events(params).filter(function(event) { return event.name == "PageView" }).groupBy(["properties.orgId"], mixpanel.reducer.count()).sortDesc("value")}' } Logger.log(data) var options = { "method": "GET", "headers" : {"Authorization" : "Basic " + Utilities.base64Encode(PropertiesService.getScriptProperties().getProperty('access_key'))}, "payload": JSON.stringify(data) }; var url = "https://mixpanel.com/api/2.0/jql" var response = UrlFetchApp.fetch(url, options); if (!response) { Logger.log("no response"); return; } var json = JSON.parse(response); var spreadsheet = SpreadsheetApp.openById(""); var sheet = spreadsheet.getSheetByName(""); sheet.clearContents(); sheet.getRange(1,1).setValue("org_id"); sheet.getRange(1,2).setValue("pv_recent_3_month"); for (var i=0; i<json.length; i++){ sheet.getRange(i+2,1).setValue(json[i].key); sheet.getRange(i+2,2).setValue(json[i].value); } }MixPanelは検索条件やデータの見え方とかをGUIで設定したreportという機能があって
それをexpoertとかもできるため、最初はそれを使おうとしたものの
どうやら、JQLをHTTP Requestとして渡すのが一番早そうだということに気づき、このやり方でやってみた同じようなことをやろうとしている方の参考になれば
類似サービスのGoogle AnalyticsだとMixPanelに比べて分析機能は弱いと感じる一方、他サービスとの連携については圧倒的。
割り切って2箇所に飛ばして、MixPanelでの分析、外部サービスとかとの連携はGoogle Analytics使うとかも一案だと思った
- 投稿日:2019-12-24T17:01:15+09:00
もう一度理解する、JavaScriptの配列とコピー
この記事はKobe University Advent Calendar 2019の23日の記事です。一日の遅刻です。いい加減に遅刻癖がつきそうな感じなので本当によくないですね、申し訳ないです。
みなさんは配列をコピーするとき、
array.slice()
を使っていますか?それとも、[...array]
でしょうか。1
本記事では、なんとなく曖昧になっているかもしれないJavaScriptでの配列のコピーについて、もう一度おさらいして理解を深めなおすことを目指します。「配列」の「コピー」とは?
そもそも、配列をコピーする、とは、どのような操作のことを指すのでしょうか。
const a = [1, 2, 3]; const b = a; // aの「コピー」??これはコピーではない、というのは、みなさんもご存知の通りのことと思います。2
記事に入るにあたって、まずはJavaScriptの配列とはどのようなものなのか、コピーするとはどういうことかを、ここでおさらいしてみることにしましょう。JavaScriptにおける配列
プリミティブとオブジェクト
JavaScriptでは、変数に入れられるものは大きく2種類に分けることができます。
それは、プリミティブとオブジェクトです。プリミティブとは、数値・文字列・真偽値・シンボル・
null
・undefined
のことを指します。3
オブジェクトとはプリミティブではないものすべてで、基本的にはObject
とそれを継承したもののことです。4配列は上に挙げたプリミティブのどれにも当てはまりませんから、配列はオブジェクトであるということになります。
具体的には、Array
オブジェクトのインスタンスのことを配列といいます。
実際、typeof array
は"object"
となり、array instanceof Object
やArray instanceof Object
はtrue
です。もう少しだけ詳細な話をします。記事の本題からやや逸れるので、興味のない方は次の節まで読み飛ばしてください。
配列はArray Exotic Objectと呼ばれ、特殊なオブジェクトの一種です。
これは、たとえば配列に要素を追加するとlength
プロパティが自動的に書き換わりますが、このような動作が仕様レベルで組み込まれていることを意味します。
逆に言えば、ほとんどの基本的な動作は通常のオブジェクトと変わりません。
ところでオブジェクトのプロパティにアクセスするとき、object.x = 1; object["x"] = 1;は全く同じ意味になる5、というのはよく知られています。
また、object["x"]
のような書き方をする際、この"x"
の部分は文字列またはシンボルのいずれかをとり6、それ以外の値が渡されたときは文字列に変換されて7からアクセスがなされます。
つまり、array[1]
と書くのは、実際にはarray["1"]
と書いているのと同じ意味で、JavaScriptの配列の添え字アクセスとは、単なるオブジェクトのプロパティアクセスにほかならないのです。イテレータ
また、配列はIterableなオブジェクトでもあります。イテラブルは言いにくいので英語で書きます。
Iterableであるとは、「繰り返し可能である」とか、「反復可能である」と訳されます。配列をfor文で回す際に、
for (const element of array) { // do something }のようにしてループをさせますが、
for of
はIterableなものを繰り返し処理するための構文です。オブジェクトがIterableであるためには、
Symbol.iterator
プロパティとしてIterator(イテレータ)を持っている必要があります。
配列の持つIteratorは、要素を先頭から順に取り出してくれるようになっています。
そのため、for of
で配列を先頭の要素から順に回していくことができます。Iteratorの詳細については、
など、詳細に説明されている良い記事が他にありますのでそちらに譲ります。
配列のほかにIterableなものとして、たとえば文字列や
Set
・Map
が挙げられます。また、普通に配列を
for of
で回した場合には配列の値だけが得られますが、インデックスと値を同時に取得したいこともあります。
Array.prototype.entries()
は、[index, value]
という形式で値を取り出してくれるIteratorを返します。
そして、for (const [index, value] of ["x", "y", "z"].entries()) { console.log(index, value); } // 0 x // 1 y // 2 zのように使うことができます。
ちょっと待ってください、
for of
で回せるのはIterableオブジェクトであって、Iteratorではなかったはずです。
Iteratorを持っているオブジェクトがIterableオブジェクトである、という関係でしたね。
実は、Array.prototype.entries()
で返されるオブジェクトは、Iteratorであると同時にIterableでもあります。
これを、IterableIteratorといいます(なんじゃそりゃ)。8
JavaScriptに組み込みで用意されているIteratorは、すべてそれ自身がIterableであるようになっています。配列のコピー
代入とコピーの違い
さて、JavaScriptの配列がどのようなものであったかを見てきましたが、これをコピーするとはどのような意味なのでしょうか。
先ほど、配列とはオブジェクトであるということをおさらいしましたが、JavaScriptでは、オブジェクトを変数に代入すると、他の言語で言うところの参照に相当するものが渡されます。
(もっとも、JavaScriptに「値渡し」や「参照渡し」という概念は存在しないので、厳密には「参照を渡す」という言葉は意味を持たないということに注意する必要があります。)const yukarin = { name: "Yukari Tamura", age: 17 }; const yukari_tamura = yukarin; yukari_tamura.age = 37; console.log(yukarin.age); // 37ここで
yukarin
とyukari_tamura
は同一の人物オブジェクトであるため、片方の変数からプロパティを変更すると、もう一方からアクセスした場合もその変更が反映された状態です。これは、オブジェクトの一種であるところの配列についても同じことがいえます。
const aegislash_shield_usum = [60, 50, 150, 50, 150, 60]; const aegislash_shield_swsh = aegislash_shield_usum; // ギルガルド(シールドフォルム)のB,D種族値は剣盾で10ずつ下方修正されました aegislash_shield_swsh[2] -= 10; // B aegislash_shield_swsh[4] -= 10; // D console.log(aegislash_shield_usum); // [60, 50, 140, 50, 140, 60] console.log(aegislash_shield_swsh); // [60, 50, 140, 50, 140, 60]
=
を使って配列を別の変数に代入した場合、代入先も代入元も同じオブジェクトですから、どちらかの要素(プロパティ)を変更するともう片方も影響を受けます。
ここで挙げた例のように、もともとの配列に入っているデータを一部改変して利用したいが、変更前のデータも引き続き利用したい、という場合に「配列のコピー」が求められることがあります。よって、配列をコピーすると言った場合、「同じ要素を持っているが別オブジェクトであるような新しい配列を作る」ことを意味する、と考えることができます。
もしも期待通りに配列がコピーできた場合、上の例では以下のような結果が期待されますね。
console.log(aegislash_shield_usum); // [60, 50, 150, 50, 150, 60] console.log(aegislash_shield_swsh); // [60, 50, 140, 50, 140, 60]浅いコピーと深いコピー
ところで、配列をコピーするという操作にも、複数の種類があります。
これは、先ほど挙げた例では問題にならないのですが、たとえば二次元配列のように、配列の中にさらに配列が入っている場合に問題となってきます。const roguelike_level01_map = [ ["|", "<", ".", "|", " ", "|", "_"], ["|", "@", ".", "+", "#", "|", ">"], ["|", ".", ".", "|", "#", "+", "."], ]; const roguelike_level02_map = myPoorCopyFunction(roguelike_level01_map); roguelike_level02_map[1][2] = "d"; // 主人公の隣にペットの犬を配置 console.log(roguelike_level01_map[1][2]); // "d"二次元配列では、配列の中にある配列もコピーしなければ、やはり同じ問題が起きてしまいます。
そのため、コピーする配列の深さを考える必要があります。配列の深さとは、配列の中の配列の中の配列…… が最大で何段階あるのかを言い表すものです。
ここでは、配列の中に配列が入っていないときは深さ $0$ 、いわゆる二次元配列では深さ $1$ とします。9
たとえば、Array.prototype.flat([depth=1])
は、配列を指定した深さ分だけ平らにならしてくれるメソッドです。
このメソッドの深さの指定を変えて試してみましょう。const peano_number_4 = [0, [0], [0, [0]], [0, [0], [0, [0]]]]; console.log(JSON.stringify(peano_number_4.flat(0))); // [0,[0],[0,[0]],[0,[0],[0,[0]]]] console.log(JSON.stringify(peano_number_4.flat(1))); // [0,0,0,[0],0,[0],[0,[0]]] console.log(JSON.stringify(peano_number_4.flat(2))); // [0,0,0,0,0,0,0,[0]] console.log(JSON.stringify(peano_number_4.flat(3))); // [0,0,0,0,0,0,0,0]
peano_number_4
の配列の一番深いところにある0
にアクセスするためには、peano_number_4[3][2][1][0]
とする必要がありますから、配列の中にさらに3段階入れ子になった配列が入っており、この場合の深さは $3$ といえます。
flat()
メソッドに3
を与えたときの結果が、ちょうどすべての配列がならされて平らになっていますね。配列をコピーする場合にも、何段階の深さだけコピーするのかによって違いがでてきます。
一般に、一番外側の配列だけをコピーして、残りは代入で済ませてしまうコピーを浅いコピー、またはシャローコピー(shallow copy)といいます。
また、配列の最も深いところまですべてコピーするものを、深いコピー、またはディープコピー(deep copy)といいます。基本的には浅いコピーで事足りることが多く、この記事で後に紹介する方法も大半が浅いコピーになります。
とはいえ、行列の計算に使う二次元配列などをコピーしたくなることもあるかもしれませんから、深さが1以上の配列をコピーする際にはコピーの深さについて考える必要がある、ということを頭に留めておくようにしましょう。配列をコピーする方法
さて、いよいよ配列をコピーする方法を見ていきましょう。
Array.prototype.slice()
,Array.prototype.concat()
ES5以前では、みなさんは主にこの方法を使われていたのではないかと思います。
slice(begin, end)
メソッドは、開始地点と終了地点を指定して、配列の中の一部分を新しい配列として取り出すものです。
引数2つをどちらも指定しなかった場合、開始地点は配列の先頭、終了地点は配列の末尾に設定されますから、もとの配列と同じ要素を持った配列が作られます。
これを利用することで、array.slice()
を呼び出すことで配列の(浅い)コピーを得ることができます。
concat(value1, value2, ...)
メソッドは、配列の後ろに要素や別の配列をくっつけた新しい配列を返すものです。
[0].concat(1,2)
は[0, 1, 2]
を返しますが、引数に配列を与えた場合はその中身が連結されるようになっているので、[0].concat([1,2])
としても[0, 1, 2]
になることに注意しましょう。
引数を何も与えなかった場合、もともとの配列になにも追加しないということですから、やはり配列の浅いコピーを得るために使うことができます。const aegislash_shield_usum = [60, 50, 150, 50, 150, 60]; const aegislash_shield_swsh = aegislash_shield_usum.slice(); aegislash_shield_swsh[2] -= 10; // B aegislash_shield_swsh[4] -= 10; // D console.log(aegislash_shield_usum); // [60, 50, 150, 50, 150, 60] console.log(aegislash_shield_swsh); // [60, 50, 140, 50, 140, 60]これらの方法を使うことで、コピー先の配列に手を加えたとしても元の配列は変化しないようにできますね。
余談ですが、これは
slice
やconcat
が非破壊的なメソッドであることを利用してコピーを行う方法です。
反対の意味として、sort
やpush
のような破壊的なメソッドが挙げられます。
破壊的なメソッドはオブジェクト(配列)に直接手を加えて変化させるもので、非破壊的なメソッドは、新しいオブジェクトを生成するなどして元のオブジェクトは変化させないものを指します。const tristar = ["神崎美月", "一ノ瀬かえで"]; const tristar_tentative = tristar.concat("紫吹蘭"); // 非破壊的操作 (もとの配列に影響しない) console.log(tristar_tentative); // ["神崎美月", "一ノ瀬かえで", "紫吹蘭"] console.log(tristar); // ["神崎美月", "一ノ瀬かえで"] tristar.push("藤堂ユリカ"); // 破壊的操作 (もとの配列に影響する) console.log(tristar_tentative); // ["神崎美月", "一ノ瀬かえで", "紫吹蘭"] console.log(tristar); // ["神崎美月", "一ノ瀬かえで", "藤堂ユリカ"]
push(x)
とconcat(x)
はともに、「配列の末尾に要素x
を付け加える」という操作ですが、push
では配列の内容が変化しているのに対して、concat
では新しい配列が返され、元の配列は変更されていないことに注意してください。
一般的には、新しい配列を作るよりはもともとの配列に値を付け加えるほうが速度が高速であるため、何度も繰り返し配列の末尾に値を追加する場合や、必ずしも配列をコピーする必要がない場合にはpush
のほうが用いられます。
似たような理由で、一般に同じ動作をするメソッドでも破壊的なメソッドの方が処理速度の観点では有利なことが多いです。10余談おわり。
[...array]
現代では、先ほど挙げた
slice()
やconcat()
を使う方法のかわりに、ES2015で追加されたスプレッド構文を利用した浅いコピーができます。
見た目がよりシンプルなので、基本的にはこれを使うのがよいと思っています。const aegislash_shield_usum = [60, 50, 150, 50, 150, 60]; const aegislash_shield_swsh = [...aegislash_shield_usum]; // 以下略
[...x]
という構文は、イテレータを使ってx
の要素を配列として展開するものです。
そのため、スプレッド構文を使うと、Iterableなオブジェクトであれば展開して配列の形にしてしまうことができます。
まあ、この記事は配列のコピーについて扱っているので、配列以外のものをコピーすることについては知ったことではありませんね。
slice()
やconcat()
を使う方法よりやや処理が遅いと言われていますが、大概の場合、こんなところでプログラム全体の動作が重くなったりはしません。つまり気にしなくてよいです。
Array.from()
Array.from()
メソッドは、ES2015で追加されたメソッドです。
Array.from(array)
とすることで、もとの配列の浅いコピーを得ることができます。
配列(または他のIterableなオブジェクト)を新しい配列の形にする、という使い方としては、上記のスプレッド構文との違いはありません。
そのため、単に配列をコピーする場合には、[...array]
としてもArray.from(array)
としても違いはないので、見た目上シンプルな[...array]
のほうを使えばよいと思います。スプレッド構文と
Array.from()
に違いが生じる場面は二つあります。まず、
Array.from()
はIterableではない配列風オブジェクト(array-like object)を配列にすることができます。11
配列風オブジェクトとは、配列のような感じだけれども配列ではないオブジェクトのことです。
具体的には、length
という名前のプロパティを持っていれば配列風オブジェクトということができます。
Array.from()
に渡されたオブジェクトがIterableではなく、かつlength
プロパティが存在していた場合に、length
プロパティの値を整数に変換し12、[0]
から順に[length-1]
までプロティアクセスすることで新しい配列が作られます。
使用例としては、Document.querySelectorAll()
などで返されるNodeList
オブジェクトがまさしく配列風オブジェクトであるため、これを配列に変換したい場合に有用です。
配列風オブジェクトを配列に変換するという用途には、ES5以前では[].slice.call(array_like)
というコードを書いていましたが、可読性でいえばArray.from(array_like)
のほうが大幅に優れているといえるでしょう。もっとも、我々は配列のコピーがしたいので、配列に似た配列でないオブジェクトのことなんて興味がありませんね。
もうひとつの違いとしては、
Array
を継承して作ったオブジェクトをサブクラスのままコピーできる、という点が挙げられます。
具体的な例を見てみましょう。class ExArray extends Array { myAwesomeMethod() { return this[0]; } } const ex_array = new ExArray(3, 2, 1); // ExArray(3) [3, 2, 1] console.log(ex_array.myAwesomeMethod()); // 3 const copy_of_ex_array = ExArray.from(ex_array); console.log(copy_of_ex_array.myAwesomeMethod()); // 3
Array
を継承して、ExArray
というクラスを作ってみました。
これは普通の配列として使えるほか、独自に定義したmyAwesomeMethod()
メソッドを呼び出すことができる素晴らしいオブジェクトです。
ここでex_array
をコピーしようとした場合、ExArray.from()
を使用します。
[...ex_array]
やArray.from(ex_array)
を用いると、ExArray
ではなくArray
のインスタンスとしての配列が得られます。
そのため、せっかくのmyAwesomeMethod()
を呼び出すことができなくなってしまいます。
ExArray
はArray
を継承している以上配列といえる気がするので、これを正しくコピーできるというのは重要ですね。
Array
を継承して作った独自の配列をコピーしたい場合には、そのfrom()
メソッドを使うということを覚えておきましょう。
JSON.parse(JSON.stringify(array))
これまで挙げてきた方法は、すべて浅いコピーを行うためのものでした。
そこで、手っ取り早く深いコピーを行うための方法として、渋々ながらJSON.stringify()
とJSON.parse()
を使った方法を紹介せざるを得ません。
JSON.stringify()
メソッドは、JavaScriptのオブジェクトを文字列の形でシリアライズするためのものです。
JSON.parse()
メソッドはその逆で、JSON文字列をJavaScriptのオブジェクトの形に変換します。深くネストした配列を一度JSON文字列の形に変換してしまい、それをもう一度配列に戻すことで、どれだけの深さがあっても深いコピーを実現することができます。
この方法の致命的な欠点として、コピー元がJSONで完全に表現可能でなければ、一度JSONを経由する過程で情報が破損してしまうことが挙げられます。
JSONでは、配列と数値・文字列・真偽値・null
、およびそれらを値として持つオブジェクトのみを表現することができます。
そのため、undefined
や関数オブジェクト、シンボルなどが配列に含まれていた場合、この方法を使ってコピーを行うことはできません。console.log(JSON.parse(JSON.stringify([undefined, function () { }, Symbol()]))); // [null, null, null]これはバグのもとになるので、数値だけの多次元配列のような、JSONへの変換を起因とする問題が起きないことが明らかな場合、かつプロトタイピングなどで手っ取り早く実装してしまいたいような場合ぐらいにしか使うべきではありません。13
深いコピーを実装する
結局のところ、簡単に配列の深いコピーを行う方法というのは、現在のJavaScriptでは提供されていません。
そのため、浅いコピーを繰り返し使用するなどの方法で、深いコピーを行う処理を実装する必要があります。// 二次元配列をコピーする function copyMatrix(base) { const result = []; for (const line of base) { result.push([...line]); } return result; } const matrix = [ [1, 0, 0], [0, 1, 0], [0, 0, 1] ]; const copy_of_matrix = copyMatrix(matrix);深いコピーが必要になる場面というのは限られてくるため、コピーしたい配列の形に応じて簡単に実装してしまえばよいでしょう。
配列の深さが予め分かっていないような場合には、入れ子になっているすべての要素を再帰的に調べていくことで深いコピーを実装できます。
const peano_number_4 = [0, [0], [0, [0]], [0, [0], [0, [0]]]]; function copyDeepArray(base) { if (!Array.isArray(base)) return base; const result = []; for (const sub of base) { result.push(copyDeepArray(sub)); } return result; } console.log(peano_number_4[3][2][1] === copyDeepArray(peano_number_4)[3][2][1]); // false2019/12/25追記:
@Yametaro さんに指摘を頂きましたが、上記の関数では、ある特定の場合に無限ループが発生してしまいます。14
それは、以下のような配列をコピーする場合です。const infinite_depth_array = []; infinite_depth_array.push(infinite_depth_array); console.log(infinite_depth_array[0][0][0][0][0][0][0][0] === infinite_depth_array); // true copyDeepArray(infinite_depth_array); // Uncaught RangeError: Maximum call stack size exceeded配列の要素の中にその配列自身が含まれている場合など、配列が有限の深さを持たない場合があり、そのようなときには深いコピーは有限回の手続きで停止しません。
うっかり正則性公理に反するような配列を投げてしまわないように注意しましょう。一度辿った配列を覚えておくことで無限ループを検知するなどの方法でエラーを防ぐことはできますが、その場合にはパフォーマンスへの影響も考慮する必要が出てくる可能性があります。
なお、JSON.stringify()
にループした配列を渡すと、Converting circular structure to JSON
エラーが発生します。おわりに
配列を複数の変数で使いまわしたい場合には、適切な方法でコピーしなければバグの原因になってしまうことがあります。
特に、配列が入れ子になっているような場合には、浅いコピーと深いコピーのどちらが適切かを正しく判断することが大切です。単純な一次元の配列のコピー、浅いコピーには
[...array]
を使いますが、配列を継承した独自のオブジェクトをコピーしたい場合には、Array.from()
(を継承したもの)を使うようにしましょう。15
深いコピーを行う場合には、専用の処理を自前で実装する必要があります。ただし、深いコピーを行うとき、それが本当に必要なのかという点については留意しておくべきでしょう。
二次元配列のようなシンプルな場合を除き、配列が入れ子になった構造が生じている時点で、それらを木構造を持ったオブジェクトなどの形で表現してしまったほうがよいことも十分に考えられます。
また、配列を何でもかんでもコピーしてしまうと、変更したと思った要素が変更されておらず、逆にバグを生み出してしまうことがあるかもしれません。配列のコピーについての基礎的な事項をしっかり理解して、思わぬバグを防ぐようにしていきましょう。
以後、
array
と書いたときには、[]
や[1,2,3]
などの任意の配列を指すこととします。 ↩よくわからない、という方も、後でもう一度説明をしますので安心してください。 ↩
https://www.ecma-international.org/ecma-262/10.0/index.html#sec-primitive-value また、近いうちにStage 4のBigIntが新しいプリミティブとして追加されると思われます。 ↩
例外として、
Object
を継承しないオブジェクトとしてObject.create(null)
で生成したものなどがあります。 ↩https://www.ecma-international.org/ecma-262/10.0/index.html#sec-property-accessors ↩
https://www.ecma-international.org/ecma-262/10.0/index.html#sec-object-type ↩
https://www.ecma-international.org/ecma-262/10.0/index.html#sec-property-accessors-runtime-semantics-evaluation ↩
もしかしたらIterableIteratorはECMAScriptの用語ではなく、TypeScriptで型付けに使われている用語だったかもしれません。 ↩
もしかしたら $1$ から数え始める場合もあるかもしれません。とりあえずこの記事では $0$ から始めています。 ↩
逆に、あらゆる変数が非破壊的であるといろいろなメリットがあるので、オブジェクトの不変性をより重視して破壊的メソッドの使用を嫌う場合もあります。 (https://immutable-js.github.io/immutable-js/) ↩
https://www.ecma-international.org/ecma-262/10.0/index.html#sec-array.from ↩
https://www.ecma-international.org/ecma-262/10.0/index.html#sec-tointeger ↩
プロトタイピングで手っ取り早くバグを埋め込むのはやめろ つまり使うな ↩
厳密には無限ループで止まらなくなるのではなく、再帰が深くなりすぎて
Maximum call stack size exceeded
エラーが発生して停止します。 ↩そもそもみんな配列を継承して新しいクラスを作ったりしませんよね なので基本的には
[...array]
だけ覚えておけば充分です。 ↩
- 投稿日:2019-12-24T15:25:00+09:00
関数とは[JavaScript本格入門]__WebデザイナーのJS学習備忘録
はじめに
Qiita3回目の投稿になります。今回は学習のJavaScriptの基本構文「関数とは」についてまとめていきます。
JavaScriptの人気書籍「JavaScript本格入門」を学習教材として使用しているのですが、かなり分厚い書籍で持ち歩くのも不便なのでいつでも見れるように内容を凝縮してQiitaに投稿して共有できればと思い、投稿することにしました。
※ この記事の内容は基本的な関数の構文に関する説明のため、私と同じようにJavaScript入門者に向けての記事となります。
関数とは
与えられたパラメーター(引数)に基づいてあらかじめ決められた処理を行い、その処理の結果を返す仕組みを関数という。
JSではデフォルトで多くの関数を提供しているが、自分で関数を定義することもできる。自分で定義した関数のことをデフォルトの関数と区別してユーザー定義関数という。ユーザー定義関数を定義するには、4つの方法がある。
1. function命令で定義する
2. Functionコンストラクター経由で定義する
3. 関数リテラル表現で定義する
4. アロー関数で定義する
function命令で定義する
● function命令 - 構文 -
script.jsfunction 関数名(引数, ...) { 実行する処理 return 戻り値; }関数名を付ける時の注意点
- (単なる文字列ではなく)識別子の条件を満たす必要がある
- 「その関数がどのような処理を実行するのか」すぐ把握できる名前を付ける
→「動詞+名詞」の形式で命名するのが一般的
→camelCase形式
名前の先頭が小文字、単語の区切りを大文字で表す記法のことで関数名はcamelCase記法で表すのが基本引数
引数は関数の挙動を決めるためのパラメーターである。
呼び出し元から指定された値を受け取るための変数を、カンマ区切りで指定する。
仮引数ともいい、関数内部でのみ参照できる。戻り値(返り値)
関数が処理の結果、最終的に呼び出し元に返すための値のことである。
通常、関数の末尾にreturn命令を記述して指定する。
関数の途中でreturn命令を記述したら、それ以降の処理は実行されない。戻り値がない(返す値がない)- 呼び出し元に値を返さない関数では、return命令を省略しても構わない。
return命令が省略された場合、関数はデフォルトでundefined(未定義)を返す。関数の定義
▼ サンプルコード
script.jsfunction getTriangle(base, height) { return base * height / 2; } console.log("三角形の面積:" getTriangle(5,2)); //結果:5定義済みの関数を呼び出す方法
script.js関数名([引数,...]);関数定義で宣言された引数(仮引数)と区別する意味で、呼び出し側の引数を実引数という。
関数の注意点
- 関数の後方の丸カッコ()は省略できない。
- 丸カッコを省略した場合、関数の定義内容がそのまま出力されてしまう。
Functionコンストラクター経由で定義する
JavaScriptには組み込みオブジェクトとしてFunctionオブジェクトを用意しており、関数はこのFunctionオブジェクトのコンストラクターを利用して定義することもできる。
● Functionコンストラクター - 構文 -
script.jsvar 変数名 = new Function(引数,... , 関数の本体);▼ サンプルコード
script.jsvar getTriangle = new Function('base','height', 'return base * height / 2'); console.log("三角形の面積:" + getTriangle(5,2)); //結果:5Functionコンストラクターには、関数が受け取る仮引数を順に並べ、最後に関数の本体を指定するのが基本
Functionコンストラクターには省略する記述方法がある
new演算子を省略してあたかもグローバル関数であるかのように記述することもできる
script.jsvar getTriangle = Function('base','height','return base * height / 2');
仮引数の部分を1つの引数として記述することもできるscript.jsvar getTriangle = new Function('base, height', 'return base * height / 2');Functionコンストラクターを使用するメリット
実は、特別な理由がない限り、あえてFunctionコンストラクターを利用するメリットはない。
だが、Functionコンストラクターはfunction命令にはない重要な特徴として「Functionコンストラクターでは、引数や関数本体を文字列として定義できる」という点がある。
▼ サンプルコード
script.jsvar param = 'height, width'; var formula = 'return height * width / 2'; var diamond = new Function(param, formula); console.log("菱形の面積:" + diamond(5,2));上記の記述方法の注意点
外部からの入力によって関数によって関数を生成した場合には、外部から任意のコードを実行できてしまう可能性がある。
Functionコンストラクターは実行時に呼び出されるたびに、コードの解析から関数オブジェクトの生成までを実行するため、実行パフォーマンス低下に影響する可能性がある。特に以下の箇所で使用することは避けるべき。
- while/for文などの繰り返しブロックの中
- 頻繁に呼び出される関数の中
重要ポイント!
JavaScriptの関数は、原則としてfunction命令、関数リテラル、アロー関数で定義する。関数リテラル表現で定義する
JavaScriptにおいて関数はデータ型の一種であり、文字列や数値と同じくリテラルとして表現できるし、関数リテラルを変数に代入したり、ある関数の引数として渡したり、戻り値として関数を返すこともできる。
※リテラルとは、プログラム上で数値や文字列など、データ型の値を書ける構文として定義したもの。
▼ サンプルコード
script.jsvar getTriangle = function(base, height) { return base * height / 2; }; console.log('三角形の面積:' + getTriangle(5,2)); //結果:5function命令と関数リテラルの違い
function命令 → 関数getTriangleを直接定義している。
関数リテラル → 「function(base, height){...}」と名前のない関数を定義した上で、変数getTriangleに格納している。関数リテラルは宣言した時点で、名前を持たないことから匿名関数または、無名関数と呼ばれることもある。
アロー関数で定義する
アロー関数はES6(ES2015)で新たに追加された記法で、関数リテラルをよりシンプルに記述できる。
● アロー関数 - 構文 -
(引数,...) => {...関数の本体...}▼ サンプルコード
script.jslet getTriangle = (base, height) => { return base * height / 2; }; console.log('三角形の面積:' + getTriangle(5,2)); //結果:5アロー関数では、functionを書かず、代わりに=>で引数と関数本体をつなぐ。
アロー関数はこの他にコードをシンプルに記述する方法がある。
本体が一文である場合には、ブロックを表す{...}を省略できる
文の戻り値がそのまま戻り値と見なされるので、return命令も省略可能。script.jslet getTriangle = (base, height) => base * height / 2;
引数が一個の場合には、引数をくくるカッコも省略可能script.jslet getCircle = radius => radius * radius * Math.PI;※Math.PIプロパティは円周率、約3.14159を表す
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/PI最後に
JavaScriptの関数の基本的な構文を理解するのは以上の4つになります。
それぞれの関数の定義の方法だけでなく、その違いや特徴、どのユーザー定義関数で関数を定義するべきなのか理解を深めていく必要があると実感しています。今回は基本的な内容について解説してきましたが、今後も学習した内容をより詳しくまとめていこうと思います。
- 投稿日:2019-12-24T14:56:24+09:00
fetchでeuc-jpページを丸ごととってきてutf-8に変換して書き出す。
fetch(`test.html`) .then(res => res.blob()) //blobとして読み込みむ .then(res => { return new Promise(resolve => { const reader = new FileReader(); reader.onload = () => { resolve(reader.result) }; reader.readAsText(res, 'euc-jp'); // 第2引数に、任意で文字エンコーディングを指定し、文字列として読み込む }); }).then(res => { const document = new DOMParser().parseFromString(res, 'text/html'); // HTMLDocument (Document) が返る console.log(document.getElementById('id名').innerText); });一度 Blob として読み込み、文字コードを指定して文字列に変換し、そこから DOM に parse する
参考
FileReader.readAsText() - 文字列として読み込む
DOMParser
Fetch API で Shift_JIS の HTML をDOM として読み込む
- 投稿日:2019-12-24T14:33:55+09:00
Nuxt.jsにTypeScriptを導入する手順まとめ
Nuxt.jsのプロジェクトにTypeScriptを導入するには、基本的に公式ドキュメント通りで問題ないのですが、ちょっとハマった箇所もあったので備忘録的にまとめておきます。
Nuxt.jsのバージョンは
2.11.0
、typescriptのバージョンは3.7.4
です。まずはNuxt.jsプロジェクトを作成
設定は適宜変更してください。
# プロジェクト作成 npx create-nuxt-app nuxt-ts-sample > Generating Nuxt.js project in nuxt-ts-sample ? Project name nuxt-ts-sample ? Project description My astounding Nuxt.js project ? Author name itouuuuuuuuu ? Choose the package manager Npm ? Choose UI framework Element ? Choose custom server framework None (Recommended) ? Choose Nuxt.js modules Axios ? Choose linting tools ESLint ? Choose test framework None ? Choose rendering mode Universal (SSR) ? Choose development tools jsconfig.json (Recommended for VS Code)typescript-buildをインストール
まずはtypescript-buildをインストールします。
npm install --save-dev @nuxt/typescript-build次に
nuxt.config.js
のbuildModules
の箇所に設定を追加します。nuxt.config.jsexport default { buildModules: ['@nuxt/typescript-build'] }typescript-runtimeをインストール
npm install --save-dev @nuxt/typescript-runtimeインストール完了後、
package.json
のscriptsを、nuxt
からnuxt-ts
に書き換えます。package.json"scripts": { "dev": "nuxt-ts", "build": "nuxt-ts build", "generate": "nuxt-ts generate", "start": "nuxt-ts start" }nuxt.config.ts の設定
nuxt.config.js
のファイル名をnuxt.config.ts
に変更します。
また、extend(config, ctx) {}
で型を指定しなければエラーになるため、指定してあげます。nuxt.config.ts.diffbuild: { - extend(config, ctx) {} + extend(config: any, ctx: any) {} }
nuxt.config.ts
に下記を追加します。nuxt.config.tstypescript: { typeCheck: true, ignoreNotFoundWarnings: true }eslint-config-typescriptをインストール
npm install --save-dev @nuxtjs/eslint-config-typescript続いて、
package.json
のscript
のlint
を修正します。package.json.diff"scripts": { - "lint": "eslint --ext .js,.vue --ignore-path .gitignore ." + "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ." }
eslintrc.js
のextends
に下記を追加します。
@nuxtjs
がある場合は削除します。eslintrc.js.diffextends: [ - '@nuxtjs', + '@nuxtjs/eslint-config-typescript', ],必要なファイルの追加
tsconfig.json
ファイルをルート直下に作成します。tsconfig.json{ "compilerOptions": { "target": "es2018", "module": "esnext", "moduleResolution": "node", "lib": [ "esnext", "esnext.asynciterable", "dom" ], "esModuleInterop": true, "allowJs": true, "sourceMap": true, "strict": true, "noEmit": true, "baseUrl": ".", "paths": { "~/*": [ "./*" ], "@/*": [ "./*" ] }, "types": [ "@types/node", "@nuxt/types" ] }, "exclude": [ "node_modules" ] }次に、
vue-shim.d.ts
ファイルをルート直下に作成します。vue-shim.d.tsdeclare module "*.vue" { import Vue from 'vue' export default Vue }Elementの問題を解決
UIフレームワークに
Element
を使用していない場合は、この項目は飛ばしても構いません。
Element
を選んでいる場合、npm run dev
を行うと下記のようなエラーが出ます。92:18 Interface 'NuxtApp' incorrectly extends interface 'Vue'. Types of property '$loading' are incompatible. Type 'NuxtLoading' is not assignable to type '(options: LoadingServiceOptions) => ElLoadingComponent'. Type 'NuxtLoading' provides no match for the signature '(options: LoadingServiceOptions): ElLoadingComponent'. 90 | } 91 | > 92 | export interface NuxtApp extends Vue { | ^ 93 | $options: NuxtAppOptions 94 | $loading: NuxtLoading 95 | context: Context ℹ Version: typescript 3.7.4 ℹ Time: 16566msこれを解決するため、
tsconfig.json
のcompilerOptions
に"skipLibCheck": true
を追加します。tsconfig.json"compilerOptions": { "skipLibCheck": true, }nuxt-property-decoratorをインストール
クラスを使用するために、nuxt-property-decoratorをインストールします。
npm install --save-dev nuxt-property-decoratornuxt-property-decoratorを有効にするために、
tsconfig.json
のcompilerOptions
に"experimentalDecorators": true
を追加します。tsconfig.json"compilerOptions": { "experimentalDecorators": true, }TypeScriptを使ってみる
pages/index.vue
のscript
の箇所をTypeScriptで書いてみます。
~/components/Logo.vue
をimportしている箇所に、.vue
をつけることに注意してください。pages/index.vue<script lang="ts"> import { Component, Vue } from 'nuxt-property-decorator' import Logo from '~/components/Logo.vue' // .vueを忘れずにつける @Component({ components: { Logo } }) export default class extends Vue { } </script>ESLintの問題を解決
linting toolsに
ESLint
を使用していない場合は、この項目は飛ばしても構いません。
ESLint
を選んでいる場合、npm run dev
を行うと下記のようなエラーが出ます。40:0 error Parsing error: Using the export keyword between a decorator and a class is not allowed. Please use `export @dec class` instead. 8 | }, 9 | }) > 10 | export default class extends Vue { } | ^ 11 | ✖ 1 problem (1 error, 0 warnings)これを解決するために、
.eslintrc.js
のbabel-eslint
を削除します。eslintrc.jsparserOptions: { parser: 'babel-eslint' // この行を削除 }確認
http://localhost:3000/にアクセスして、下記の様な画面が確認できれば完了です!
お疲れ様でした!
- 投稿日:2019-12-24T13:59:32+09:00
11. タブをスペースに置換
11. タブをスペースに置換
タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.
Go
package main import ( "bufio" "fmt" "os" "strings" ) func main() { // 読み込みファイルを指定 name := "../hightemp.txt" // 読み込むファイルを開く f, err := os.Open(name) if err != nil { fmt.Printf("os.Open: %#v\n",err) return } defer f.Close() // 終了時にクリーズ // スキャナライブラリを作成 scanner := bufio.NewScanner(f) // データを1行読み込み for scanner.Scan() { // TAB を 空白へ置換 fmt.Println(strings.Replace(scanner.Text(),"\t"," ",-1)) } // エラーが有ったかチェック if err = scanner.Err(); err != nil { fmt.Printf("scanner.Err: %#v\n",err) return } }python
# ファイルを開く with open("../hightemp.txt", "r") as f: # 一行ずつ読み込む for data in f: # TAB を 空白へ置換(strip で white space を除去) print(data.strip().replace("\t"," "))Javascript
// モジュールの読み込み var fs = require("fs"); var readline = require("readline"); // ストリームを作成 var stream = fs.createReadStream("../hightemp.txt", "utf8"); // readlineにStreamを渡す var reader = readline.createInterface({ input: stream }); // 行読み込みコールバック reader.on("line", (data) => { // TAB を 空白へ変換(文字列 "\t" 指定ではうまく動作しないため正規表現で指定) console.log(data.replace(/\t/g," ")) });まとめ
Javascirpt で置換元文字列に "\t" が指定出来ないのか?。
Python コード数の少なさに改めて驚く。
- 投稿日:2019-12-24T11:33:54+09:00
ネイティブのJavaScriptばっかり使ってた人のためのvue.js超入門
三度の飯よりJavaScript(大嘘)。
どうもなっかのうです。
今回はJavascriptについて話します。
※ネタ要素マシマシなので、胃がもたれやすい方は気をつけてください。
読んで欲しい人
- HTML/CSS/JSをちょっと理解してる人
- おふざけ嫌いじゃないよって人
「Vue.js」って誰?親戚にいたような...
Vue.jsは人ではありません。
JavaScriptのライブラリ、フレームワークです。
jQueryとかネイティブのJavaScriptでは結構手間のかかることも割と簡単にしてくれます。
Vue.jsは主にフロントエンドの開発に使うもので、
「DOM」と呼ばれるプログラムからHTMLを操るやつを自動的に行ってくれるそうです。
なんかすごいね。(わかってない)
どうやったら使えるのかね?
HTMLのheadタグのところに、
index.html<head> <!-- 省略 --> <script src="https://cdn.jsdelivr.net/npm/vue"></script> </head>と書きます。
これで下ごしらえは完了です。
本題です
今回は、
<input type="text">
の中身がHTMLにすぐさま更新されるものを作っていきます。それではソースコードです。
index.html<div id="app"> <h2>最優秀賞 <p>{{ message }}</p> </h2> <p>({{ name }})</p> <input v-model="message"> <input v-model="name"> </div>script.jsvar app = new Vue({ el: '#app', data: { message: '赤信号は止まらないと ダメよ〜 ダメダメ', name: 'ゆでたまご小学校 6年 エレキテル太郎くん' } })これだけであの動きができるようになります!すごいでしょ!
(内容がツッコミどころ満載ですが、許してくださいな)
ちなみに、この動作のキモとなるのは
<input v-model="name">
です。「v-model」がこのリアルタイムで書き換える動作をやってくれます。
そして、
{{ message }}
というのがVue.jsによって書き換えられる部分です。その書き換えられるデータは、script.jsの
data:{}
に書いてあります。Vueの最初の3ステップ
{{ message }}
で場所を作る!
el: 'id,class名'
で選択!
data:{ message:"書き換えたい文字"}
で置き換える!これでオーケーです!
みなさんも楽しいVue.jsライフを楽しみましょう!
- 投稿日:2019-12-24T09:40:06+09:00
SvelteでSwipe Card UIを作ったので、所感をまとめます
この記事はJavaScript 2 Advent Calendar 2019、24日目の記事です。
Svelteを使って、ブラウザで動作するSwipe Card UIを作りました。
スマホにしか対応していませんが、こちらにデプロイしてあるので、興味がある方は触ってみてください。(ソースコードはgithubで公開しています)
はじめに:Svelteとは
- Write less code
- No virtual DOM
- Truly reactive
の三拍子をアピールポイントにしている、Javascriptフレームワーク(正確にはコンパイラ)です。
最新バージョンはv3。
「React,Vue,Svelte」という立ち位置を目指しています。
日本語の記事を色々探るより、
公式のHPと、作者が登壇した際の動画を見るのが一番分かりやすくて詳しいと思います。Svelteのコードの書き方の基本については、Write less code
に出てくるサンプルコードを見れば、ひと目わかると思います。
画像になりますが、貼っておきます。コード量はたしかに少ないですね。
(v2まではかなり書き方が違ったようです。)この記事で言及すること
今回僕が作ったのは、tinderなどのマッチングアプリでよく見る、Swipe Card UIもどきです。
Svelte流の書き心地を確かめるため、サンプルとして作成してみました。少々雑ですが、スワイプすれば何枚かごとに写真が切り替わります。
(写真が切り替わる時に、移動中のカードの写真も変わっちゃいますが許して)
この記事では、実際に作成する中で気になった、Svelteの特徴の中から、以下の項目について詳しく記述します。
- DOMイベント/カスタムイベントの管理
- テンプレートでのロジック分岐
- UIのアニメーションに必要な動きの計算
DOMイベント/カスタムイベントの管理
Swipe Card UIでは、ユーザーの操作によるタッチイベントを監視します。
そのイベントに伴い、画面に描画しているカードを移動させる必要があるからです。SvelteでネイティブDOMイベントを扱うのは簡単です。
例えば、要素をクリックした時にhoge
関数を実行したい場合、以下のようにイベントハンドラを記述します。<div on:click={hoge}></div>vueの
v-on
デイレクティブとほぼ同じですね。
それと似たようにイベント修飾子も存在していて、<div on:click|once={hoge}></div>こんな感じでDOMイベントに制限をもたせたり、オプションを加えることが出来ます。
より詳しくはチュートリアルをご覧くださいませ。
Svelte Tutorial Event modifiersイベントハンドラについてSvelteで面白と思ったのは、ここに開発者が定義するカスタムイベントを指定できることです。
今回僕が作成したSwipeCardのコードを見ると、以下のようにイベントハンドラを記述しています。
(イベントハンドラ以外は省略)<div use:swipe on:swipestart={handleSwipeStart} on:swipemove={handleSwipeMove} on:swipeend={handleSwipeEnd} />ここで設定している
swipestart
やswipemove
は、DOMのネイティブイベントではありません。
これは、use
ディレクティブに指定する関数の中で作成している独自のイベントです。
use
ディレクティブで呼び出しているswipe関数でswipestart
を定義している部分を抜粋すると、以下の部分になります。export const swipe = node => { let x; let y; const handleTouchstart = event => { x = event.touches[0].clientX; y = event.touches[0].clientY; node.dispatchEvent( new CustomEvent("swipestart", { detail: { x, y } }) ); } node.addEventListener("touchstart", handleTouchstart); return { destroy() { node.removeEventListener("touchstart", handleTouchstart); } }; }引数で取得するDOMノードに対し、
addEventListener
を用いて
ネイティブイベントが呼び出された時に特定のCustomEvent
をdispatchEvent
で発火させています。
関数の返り値として、removeEventLitener
が設定されているので、余計なイベントの監視はありません。(
CustomEvent
やdispatchEvent
はSvelte特有のメソッドではありませんが)
このように独自のイベントを結びつけることができます。
今回のサンプルアプリではそれほど真価を発揮していませんが、
サードパーティのライブラリを結びつけたり、複数のイベントをまとめて管理したい時に
価値を発揮するのかな?と思っています。テンプレートでのロジック分岐
Svelteで、HTML部分に出し分けを実装する場合は以下のようになります。
{#if boolean} <p>hoge</p> {:else} <p>fuga</p> {/if}これはVue,Reactの書き方とも大きく異なります。
HTMLのテンプレートエンジンや、PHPのコードを書く感覚に近いと思いました。
ifブロックの他にも、eachで配列を回したり、awaitを使って表示を出し分けたりするブロックがあります。ブロックの始まりは
#
、継続は:
、終わりは/
でかき分けるのですが、
慣れるまでは違和感を感じそうです。UIのアニメーションに必要な動きの計算
Svelteでは、UIの異なる2つの状態を滑らかにつなげるための機能があります。
Tweenedと、Springです。
Tweened
を使うとCSSのanimation
プロパティで設定することが多かった
duration
やeasing
の計算をJS内で完結することができます。
Spring
では、変化の「stiffiness(かたさ)」や「damping(跳ね返り)」も含めた定義が可能です。
Swipe Card UIには、Spring
をピンポイントで使用しています。
特に便利だったのは、タップをやめてカードが元の位置に戻る際のdamping
です。これらの機能が行うのは変数の変化までなので、その値をUIと結びつける必要があるのですが、
今回はインラインstyle内で変数を使用して結びつけました。おわりに
今回使った機能の他にも、アプリ内でグローバルに値を管理するためのStore、
propsの受け渡しをなくすためのContext APIなど、Svelteのコードにはいくつも特徴的な点があります。
公式のチュートリアルによくまとまっているので、
気になる方は触ってみてください!個人的に、
Vueが好きな人はとっつきやすいかもしれませんが、
Reactが好きな方にはあまり好かれなさそうだな、と思いました。ReactやVueと何がいいんだ、という点についての詳細は、
画面遷移や実際にアプリとして使用できる機能を持つwebアプリを作った時に振り返ります。
(サンプルケースだけで比較している記事はよくあるのですが、それだけで実務で通用するか否かは分からないと思っている)1月中に作れるかな!
進捗は多分、twitterでつぶやくと思います。ちなみに:コンパイル後の容量
Svelteは、コンパイル後のコードにはvanillaJSしか含まれず、軽量なことをアピールしています。
今回実際に作成したファイルの容量は・・・25KB!
確かに軽い。
- 投稿日:2019-12-24T09:34:59+09:00
Babel Plugin を作りながら AST と Babel を学ぶ
この記事は JavaScript Advent Calendar 2019 の 23日目の記事です。
前日の22日目は Vue-CLI 4を使用したJavaScript開発環境構築(プロトタイプ版とプロジェクト版) でした。今回は表題通り
Babel Plugin
を作りながらAST
とBabelを学ぼうという記事です。AST とは?
まずは根幹である
AST
について軽く説明します。
AST
はAbstract Syntax Tree
の略で、日本語では抽象構文木などと呼ばれるものです。
AST
はプログラムの構造を示したデータ構造体であり、JavaScriptではJSON
データの形で表現されることが一般的になり、基本的に仕様は、ESTree に準拠されています。
AST
はBabel
以外に ES Lint や webpack などにも使用されています。実際に
AST
がどのようなものなのかをAST explorerというサイトで簡単に確認することができます。
今回はconst a = 1
をAST
の構造体にしてみました。画面左側が実際の値、右側が
AST
の構造体になります。
これは acorn というミニマムな JavaScript の parser により生成されたものです。こちらがそのデータになります。
{ "type": "Program", "start": 0, "end": 11, "body": [ { "type": "VariableDeclaration", "start": 0, "end": 11, "declarations": [ { "type": "VariableDeclarator", "start": 6, "end": 11, "id": { "type": "Identifier", "start": 6, "end": 7, "name": "a" }, "init": { "type": "Literal", "start": 10, "end": 11, "value": 1, "raw": "1" } } ], "kind": "const" } ], "sourceType": "module" }後ほど詳しく説明します。
今はAST
はこのようなJSON
なんだなというくらいの理解で大丈夫です。Babel とは?
続いて
Babel
について説明をします。
Babel
とは JavaScript のコードを変換してくれる Compiler です。元々
Babel
は6 to 5
という名称でした、その名の通り ES6 から ES5 にコードを変換するだけのものでした。
しかし、その後さまざまな要望を得た機能が実装され、現在のBabel
という名称になりました。Babelの機能
Babel
は主に3つの機能を備えています。
- 構文変換
- Polyfill の提供
- ソースコードの変換
Babel
は上記のような機能をもって、ブラウザでは使用できない最新の機能を書いた JavaScript や TypeScript を、指定したブラウザでも使用できるようにコードを変換します。例えば
const a = () => {}
のようなアロー関数は、@babel/plugin-transform-arrow-functions によって処理され、このようなコードになります。https://babeljs.io/docs/en/babel-plugin-transform-arrow-functions
Babel はコードをどのように変換するのか?
Babel
の変換にはこのように3つの段階があります。
- Parsing
- @babel/parser を用いて、ソースコードを
AST
に変換- Transformation
- @babel/traverseを用いて、
AST
を変換する、- Code Generat
- @babel/generator を用いて
AST
をソースコードに変換する実際に Babel Plugin を作りながら AST を学ぶ
ここからは実際に
Babel Plugin
を作りながらBabel
がどのようにAST
を駆使してコードを変換しているのか見ていきましょう。
今回はconst
とlet
をすべてvar
に置き換えるものを作成します。前準備
必要とならパッケージを事前に落としておきます。
npm i -D @babel/parser @babel/generator @babel/traverse
1. Parse
@babel/parser を使用して、ソースコードを
AST
に変換しましよう。// parser/index.js const { parse } = require("@babel/parser"); // AST に変換 const ast = parse(` const a = 1 `); console.log(JSON.stringify(ast, null, 2));このコードを実際に
.json
ファイルに出力します。node node parser/index.js実際に出力されたものはこちらです。
{ "type": "File", "start": 0, "end": 13, "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 3, "column": 0 } }, "errors": [], "program": { "type": "Program", "start": 0, "end": 13, "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 3, "column": 0 } }, "sourceType": "script", "interpreter": null, "body": [ { "type": "VariableDeclaration", "start": 1, "end": 12, "loc": { "start": { "line": 2, "column": 0 }, "end": { "line": 2, "column": 11 } }, "declarations": [ { "type": "VariableDeclarator", "start": 7, "end": 12, "loc": { "start": { "line": 2, "column": 6 }, "end": { "line": 2, "column": 11 } }, "id": { "type": "Identifier", "start": 7, "end": 8, "loc": { "start": { "line": 2, "column": 6 }, "end": { "line": 2, "column": 7 }, "identifierName": "a" }, "name": "a" }, "init": { "type": "NumericLiteral", "start": 11, "end": 12, "loc": { "start": { "line": 2, "column": 10 }, "end": { "line": 2, "column": 11 } }, "extra": { "rawValue": 1, "raw": "1" }, "value": 1 } } ], "kind": "const" } ], "directives": [] }, "comments": [] }これで
AST
に変換することができました!2. Generate
続いては @babel/generator を用いて先ほど
AST
にしたデータをソースコードに変換します。// generator/index.js const { parse } = require("@babel/parser"); const generate = require("@babel/generator").default; // ソースコードを AST に変換 const ast = parse(` const a = 1 `); // generate の第一引数に AST を格納 console.log(generate(ast).code);このコードを実行してみましょう。
node generator/index.js実行結果はこのようになります。
const a = 1;
AST
に変更される前と変わりないコードが生成されました、これはAST
になにも変更を加えずにコードに戻したからです。3.Travers
ここから本題である
const
とlet
をすべてvar
に置き換える作業を行います。
もう一度1. Parse
で吐き出したAST
を見てましょう。{ "type": "File", "start": 0, "end": 13, "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 3, "column": 0 } }, "errors": [], "program": { "type": "Program", "start": 0, "end": 13, "loc": { "start": { "line": 1, "column": 0 }, "end": { "line": 3, "column": 0 } }, "sourceType": "script", "interpreter": null, "body": [ { "type": "VariableDeclaration", "start": 1, "end": 12, "loc": { "start": { "line": 2, "column": 0 }, "end": { "line": 2, "column": 11 } }, "declarations": [ { "type": "VariableDeclarator", "start": 7, "end": 12, "loc": { "start": { "line": 2, "column": 6 }, "end": { "line": 2, "column": 11 } }, "id": { "type": "Identifier", "start": 7, "end": 8, "loc": { "start": { "line": 2, "column": 6 }, "end": { "line": 2, "column": 7 }, "identifierName": "a" }, "name": "a" }, "init": { "type": "NumericLiteral", "start": 11, "end": 12, "loc": { "start": { "line": 2, "column": 10 }, "end": { "line": 2, "column": 11 } }, "extra": { "rawValue": 1, "raw": "1" }, "value": 1 } } ], "kind": "const" } ], "directives": [] }, "comments": [] }
VariableDeclaration
に"kind": "const"
という値があるのが分かります。
ここがかなり怪しいので、冒頭にも紹介した JavaScript のAST
の仕様に相当するEsTree
でVariableDeclaration
を検索してみましょう。
検索結果はこちらこのような検索結果コードが表示されています。
extend interface VariableDeclaration { kind: "var" | "let" | "const"; }どうやらこの
kind
の値をvar
にすればよさそうなので、これからASTを使って変換していきます。
今回は @babel/traverse を使用していきます。// traverse/index.js const { parse } = require("@babel/parser"); const generate = require("@babel/generator").default; const traverse = require("@babel/traverse").default; // AST を変換 const ast = parse(` const a = 1 let b = 2 `); // AST を第一引数に、変更内容を第二引数にする traverse(ast, { // 変更したい部分(今回は VariableDeclaration ) VariableDeclaration(path) { // kind を var に変更 path.node.kind = "var"; }, }); // // generate の第一引数に AST を格納し、コードを生成 console.log(generate(ast).code);これを実行します。
node traverse/index.js実行結果はこちらになります。
var a = 1; var b = 2;これで、
const
とlet
をvar
に変更することができました。ほかにもこのようにすることで値の変更もできます。
// traverse/index.js const { parse } = require("@babel/parser"); const generate = require("@babel/generator").default; const traverse = require("@babel/traverse").default; const ast = parse(` const a = 1 let b = 2 `); traverse(ast, { VariableDeclaration(path) { path.node.kind = "var"; }, VariableDeclarator(path) { if (path.node.id.name === "a") { path.node.id.name = "c"; path.node.init.value = 3; } } }); console.log(generate(ast).code);実行。
node traverse/index.js実行結果。
var c = 3; var b = 2;このように
AST
を駆使すれば正規表現では表現できないようなパターンも変更可能になります。最後に
今回説明した部分は、
Babel Plugin
の肝となる部分になります。
AST
とBabel Plugin
の肝を理解すれば、 @babel/plugin-transform-arrow-functions のコード のようなプラグインがやっていることは案外簡単なことのように思えるかもしれません。
AST
を使って便利ツールを作っていきましょう!!今回使用したソースコードはこちらのレポジトリにあります。
https://github.com/sakito21/babel-plugin-demo
- 投稿日:2019-12-24T09:29:41+09:00
CesiumでSRTMの地形データを読み込ませてみる
概要
SENSYN Robotics(センシンロボティクス)の深見です。
主にWebアプリを中心に開発担当しており、最近はCesiumを利用したWebGL系の開発に関わっております。
本日は、その過程で得た、地形データをCesium上に反映させる方法につきまして紹介いたします。
CesiumについてはこちらCesiumに反映する地形データの取得
- Cesium上で地形データを反映させるためにはいくつか手段はありますが、ここではCesium World Terrain と同じく
quantized mesh
で配信する事例とします。- 今回利用する地形データはNasa SRTM(height map)を利用します。
- 地形データをダウンロード時には認証が求めらるので、こちらでログインIDを取得する必要があります。
- また、データ量が多いため、今回の対象は関東エリア周辺(N35E139.hgt)のみといたします。
Cesium Terrain Builder
- Cesium向けにterrainを生成するためcesium-terrain-builderを利用します。
地形データの作成
- cesium-terrain-builderを利用するため、docker imageを取得します。
$docker pull tumgis/ctb-quantized-mesh
- 事前にダウンロードしたheight map(N35E139.hgt)を配置します。
$ mkdir ~/terrain $ mv ~/Downloads/N35E139.hgt ~/terrain
- dockerコンテナを起動します。
$ docker run -it --name ctb -v ~/terrain/:/data tumgis/ctb-quantized-mesh
- gdalbuilderで仮想的なレイヤを定義するXMLファイル(vrt)を作成します。また、Cesium上では地形データをquantized meshで展開するため、ファイルを作成します。
$ gdalbuildvrt tiles.vrt *.hgt $ ctb-tile -f Mesh -C -N -o terrain tiles.vrt
- Cesiumが地形データを認識するための設定ファイルとして、layer.jsonを作成します。
$ ctb-tile -f Mesh -C -N -o -l terrain tiles.vrt
- 地形データとlayer.jsonが作成されているか下記コマンドで確認します。
$ tree -v -C -L 1 ~/terrain terrain/ |-- 0 |-- 1 |-- 2 |-- 3 |-- 4 |-- 5 |-- 6 |-- 7 |-- 8 |-- 9 |-- 10 |-- 11 |-- 12 `-- layer.json以上で地形データの作成は完了です。
地形データの配信
- webアクセスできる環境を用意します。配信する際にresponseヘッダーの確認が必要です。配信するサーバの環境に合わせ確認してください。
![]()
- Access-Control-Allow-Origin
- corsで許可して配信する場合には必要、上記の例は特別な理由がないので
Access-Control-Allow-Origin: *
としています。- Content-Encording
ctb-tile
のコマンドで作成された地形データはgzip形式で圧縮
されているため、配信する場合はContent-Encording: gzip
を指定します。- Content-Type
application/octet-stream
を指定します。cesium-terrain-builderで作成されたファイルが.terrain
の拡張子ファイルであり、一致するMIMEtypeが存在しないので、Content-Type: application/octet-stream
とします。- 今回はpythonのSimpleHTTPServerを利用してweb配信します。
$ python SimpleHTTPServer.py Serving HTTP on 0.0.0.0 port 8000 ...
Cesiumに地形データを読み込ませる
- CesiumのterrainProvidorにurlを設定します。
const viewer = new Cesium.Viewer('cesium', { terrainProvider : new Cesium.CesiumTerrainProvider({ url : 'http://localhost:8000/terrain' }), });ブラウザで確認
以上になります。
- 投稿日:2019-12-24T09:11:25+09:00
JavaScriptで動画再生を操作する
動画の設置
<video>
要素の記述htmlの基本的な記述は以下の通り。
<video>
要素のsrc
属性で動画のパスを指定します。動画再生に非対応の古いブラウザ用に、代替テキストや画像を用意するとよいと言われています。その場合は、<video>
要素内に記述します。index.html<video src="./hoge.mp4"> <p>ご使用のブラウザでは動画再生に対応していません</p> </video>HTML5以降では、
<video>
要素内に<source>
要素を記述し、複数のソースを指定することが可能です。その場合は、上から順に<source>
をみていき、再生可能なものが利用されます。index.html<video> <source src="./hoge.mp4" type="video/mp4"> <source src="./hoge.webm" type="video/webm"> <p>ご使用のブラウザでは動画再生に対応していません</p> </video>
<video>
要素に指定できる属性
<video>
要素のcontrols
属性を使用することで、ブラウザに用意されたコントローラを使用することができます。インターフェースはブラウザに依存します。
コントローラーは使用せず、JavaScriptで操作する場合の記述は、次のセクションにまとめています。index.html<video src="./hoge.mp4" controls> <p>ご使用のブラウザでは動画再生に対応していません</p> </video>その他、
<video>
要素には、以下の属性を指定できます。
属性 機能 値 autoplay ロードされたら自動的に再生を開始 なし loop ループ再生する なし muted デフォルトで音量をゼロにする なし preload データのプリロードについての指定 "none"/"metadata"/"auto" poster データが再生可能になるまでに表示させる画像 画像のパス width 高さ 整数値 height 幅 整数値 参考: https://developer.mozilla.org/ja/docs/Web/HTML/Element/video
JavaScriptでの動画再生
JavaScriptで動画を操作する場合の主要な記述を以下にまとめています。
動画の再生操作
index.html<video id="video" src="./hoge.mp4"> <p>ご使用のブラウザでは動画再生に対応していません</p> </video>main.jsvar v = document.getElementById('video'); //再生 v.play(); //一時停止 v.pause(); //ロード v.load();play()
paused
属性をfalse
に設定する、必要に応じリソースをロードする、再生を開始するpause()
再生を中断する、
paused
属性をtrue
に設定する、必要に応じリソースをロードするload()
要素をリセットする、新たなリソースを選択しロードを開始する
JavaScriptでの再生位置の取得
main.js//再生位置の取得 v.addEventListener('timeupdate', function() { if (v.currentTime !== 0) { console.log(timeConvert(v.currentTime)); } else { console.log('0:00'); } }) //数値型から”00:00”表記への変換(秒、ミリ秒の場合) function timeConvert(time) { var sec = Math.floor(time); var msec = ((time - sec) * 100).toFixed(0); return sec + ':' + msec; }currentTimeプロパティ
再生位置を数値型データ(秒単位)で返す
durationプロパティ
メディアの再生時間を数値型データ(秒単位)で返す
再生状態に関するプロパティ
played
: 再生が完了した時間の長さを表すpaused
: 再生が一時停止されているかどうかをBoolean値で表すended
: 再生が終了しているかどうかをBoolean値で表すerror
: 直近で発生したメディアのダウンロード中のエラーをerror
オブジェクトで返す再生の設定に関するプロパティ
loop
: 繰り返し再生の有効・無効をBoolean値で表すcontrols
: 再生をコントロールするユーザインタフェースの表示・非表示をBoolean値で表すpreload
: メディアをプリロードすべきか指定する、none
、metadata
、auto
のいずれかの値をとるautoplay
: htmlのautoplay
属性を反映し、Boolean値で表す再生状態に関するイベントハンドラ
play
: 再生が開始したときtimeupdate
: 再生位置が変化したときpause
: 再生が中断したときplaying
: 再生中断状態から、ふたたび再生可能になったときwaiting
: 次のフレームの受信を待っているときended
: 再生が完了したときerror
: 再生中にエラーが発生したときabort
: エラー以外の原因で再生が停止したときメディアの読み込みステータスの取得
readyStateプロパティ
メディアリソースの読み込みの状態を取得する。戻り値は以下の通り。
0
:HAVE_NOTHING
リソースに関するいかなる情報も利用できない状態
1
: HAVE_METADATA
リソースの情報(ビデオ要素の場合、ビデオの高さ・幅など)を取得済み、再生位置に関するデータは未取得
2
: HAVE_CURRENT_DATA
再生位置に関するデータは取得済み、再生位置より先のフレームデータは未取得
3:
HAVE_FUTURE_DATA
再生位置より先のフレームデータも取得済みで、早送りができる状態
4
: DONE(完了)
3
の状態で、なおかつ、十分なデータがロードできていて、このまま再生しても、再生位置が読み込みデータを追い越さないような状態networkStateプロパティ
メディアリソースの読み込みのネットワークの状態を取得する。戻り値は以下の通り。
0
: NETWORK_EMPTY
リソースに関するいかなる情報も利用できない状態
(redayState
プロパティのHAVE_NOTHINGと同じ状態)
1
: NETWORK_EMPTY
要素がアクティブではあるが、ネットワークは使用されていない状態 (リソースが取得できている)
2
: NETWORK_LOADING
リソースの読み込み中
3
: NETWORK_NO_SOURCE
リソースが見つからないロード状態に関するイベントハンドラ
index.html<video id="video"> <source src="./hoge.mp4" type="video/mp4"> <p>ご使用のブラウザでは動画再生に対応していません</p> </video> <p id="state"></p>main.jsdocument.addEventListener('DOMContentLoaded', function() { var v = document.getElementById('video'); var state = document.getElementById('state'); //ロード開始 v.addEventListener('loadedmetadata', function() { state.textContent = 'ロードを開始しました'; }) //読み込み完了 v.addEventListener('loadeddata', function() { state.textContent = '読み込み完了しました'; }) //再生可能 v.addEventListener('canplay', function() { state.textContent = '再生可能です'; }) //再生中 v.addEventListener('playing', function() { state.textContent = '再生中です'; })参考
https://developer.mozilla.org/ja/docs/Web/HTML/Element/video
https://developer.mozilla.org/ja/docs/Web/API/HTMLMediaElement
- 投稿日:2019-12-24T09:10:22+09:00
Web Worker� でJavaScriptを並列処理させる
JavaScript のWeb Worker API の使い方についての覚え書きです。
Web Worker とは
Web Workerは、JavaScriptの処理を、バックグランドで並列に実行するための機能。
Workerへの処理命令
呼び出し元の記述例
main.jsから、Worker(worker.js)を呼び出し、処理を投げます。
index.html<label for="input1">width: </label><input id="input1" type="number"> <label for="input2">height: </label><input id="input2" type="number"> <input id="btn" type="button" value="Area?"> <output id="output"></output>main.jsdocument.addEventListener('DOMContentLoaded', function () { document.getElementById('btn').addEventListener('click', function () { let width = document.getElementById('input1').value; let height = document.getElementById('input2').value; let output = document.getElementById('output'); //Workerオブジェクト var worker = new Worker('url_hoge/worker.js'); //workerへ処理を命令 worker.postMessage({ 'width': width, 'height': height }); //Workerから処理結果を受信したときの処理 worker.addEventListener('message', function (e) { output.textContent = e.data; }, false); //Workerからエラーを受信したときの処理 worker.addEventListener('error', function (e) { output.textContent = e.message; }, false); }) })
- コンストラクタ
Worker()
の引数で、worker側のjsのパスを指定。postMessage()
の引数がworker.jsへ渡される。引数には文字列しか取れないので、オブジェクトJSONに変換してから渡す。引数にはJavaScriptのあらゆるオブジェクトを指定可能。- worker.js側からの処理が返ると、
message
イベントが発火する。このイベントオブジェクトのdata
プロパティとして、処理結果が返される。Worker側の記述例
全然重たい処理ではないけど、Worker側でJavaScriptを実行させてみます。
worker.jsself.addEventListener('message', function (e) { if (!Number(e.data.width) || !Number(e.data.height)) { self.postMessage('入力エラーです'); } else { self.postMessage(Number(e.data.width) * Number(e.data.height)); } })
- main.js側からの命令を受けると、worker.js側で
message
イベントが発火する。- main.jsから
postMessage()
の引数として渡されたデータが、イベントオブジェクトのdata
プロパティに入る。postMessage()
で、main.jsに処理結果を返す。postMessage()
呼び出し元からWorkerへ処理を命令する、あるいは、Workerから呼び出し元へ処理結果を返す。
引数は、メッセージを受け取った側からは、message
イベントのdata
プロパティでアクセスできる。messageイベント
Worker側で呼び出し元からの処理命令を受け取ったとき、あるいは、呼び出し元側でWorkerからの処理結果を受け取ったときに発生するイベント。
errorイベント
Worker側で実行時にエラーが発生したときに発生するイベント。
エラーイベントは以下のプロパティをもつ。
message
人間が読み取れるエラーメッセージfilename
エラーが発生したスクリプトのファイル名lineno
スクリプトファイル内でエラーが発生した場所の行番号Workerの終了
terminate()
worker.terminate();呼び出し側からWorkerを終了させる。Workerは、自身の操作を完了したり、クリーンアップしたりすることなく、直ちに終了する。
close()
Worker側から自分自身を終了する。
参考
- 投稿日:2019-12-24T09:09:08+09:00
JavaScriptでブラウザの接続状態を取得する
window.navigator.onLineプロパティ
ブラウザの接続状態を真偽値で表すプロパティ。
true
: 接続されているfalse
: 接続されていないユーザがリンクをたどる、もしくは、スクリプトがリモートページを要求したときに値が更新される。
例えば、閲覧しているページからユーザがリンクをクリックした時に、インターネットへの接続が切れてていると、プロパティはfalse
を返す。ブラウザによる実装状況
Chrome, Safari
false
: ブラウザがLANまたはルータに接続できないときtrue
: 上記以外(必ずしも、インターネットにアクセスできる状態とは限らない)Firefox, IE
false
: ブラウザがオフラインモードにあるときtrue
: 上記以外*irefox 41以降では実際のネットワーク接続状態に従って値を返す
online / offline イベント
onLine
プロパティの値更新に準じて、Windowに対しイベントが発生する。
onLine
プロパティと同様、必ずしも、インターネットにアクセスできる状態であるかを示す値ではないので注意する。//オンラインになった時 window.ononLine(function() { //処理内容 ) //オフラインになった時 window.onoffLine(function() { //処理内容 )参考
https://developer.mozilla.org/ja/docs/Web/API/NavigatorOnLine/onLine
- 投稿日:2019-12-24T08:55:29+09:00
画像ファイルが選択されたらブラウザ上でリサイズしてBase64文字列にして結果をフォームに埋め込んでサーバに送信する(非ajax)
色々と面倒だったので備忘。
アプリケーション設定はplayframework
独自ですが、フロント側は素のjavascript
でいけますし、サーバサイドもどんな言語・Webフレームワークでも道具立てが用意されているはずです。
- アプリケーション設定
application.conf
でBodyParserのデータ上限を緩和する.- HTML
- input[type='file']タグを用意
- 上記に対応するcanvasを用意する。表示サイズはwidth指定しておく。(役割はファイル選択時のサムネイル表示とBase64文字列取得)
- 画像ファイル選択
- 選択されたら、canvasを初期化する(clearRect)。ファイル内容を読み込む。ファイル種別等をチェック。
- ファイル読み込みが完了(onload)したらその結果(e.target.result)をimg.srcに設定する
- img.srcの読み込みが完了(onload)したら、その内容を適宜リサイズしてcanvasに描画(drawImage)する
- フォーム確定(submit)
- フロントエンド
- 選択されたファイルが存在する場合、canvasからBase64化された文字列を取得(
toDataURL('image/jpeg')
)。以降ではイメージ文字列と呼称- フォームにhidden属性を追加して値にイメージ文字列を設定する
input[type='file']
に設定された内容を消す ← 無駄なので。そもそも画像リサイズ要件の背景は「通信遅い」から派生しているので- submitする
- サーバサイド
- multipartでなく通常のフォームパラメタとして読み出す
- イメージ文字列のデータ部分を切り出して、BufferedImageに変換する。=> このあとはファイルに戻したり、好きに加工
data:image/jpeg;base64,★ここから後=>★/9j/4AAQSkZJRgABAQEASABIAAD/4ge4SUNDX1BST0ZJTEUAAQEAAAeoYFKc5KKbP/2Q==
ポイントとしては、
- サーバサイドに送信するデータは
canvas
に描画したデータで、input[type='file']
のデータではないこと- 非ajax(フォームに埋め込んでサーバに送信)であること。(こういった処理を行う場合は、Blob化して
FormData
に詰めて、ajaxでサーバサイドに送ることが多い模様)参考
- 投稿日:2019-12-24T07:52:19+09:00
GASで「イレギュラーな予定に対応できる」そんなノート分類システム
What I wanted to do?
僕の塾では、以前記事で書いたとおり、塾のノートを一度写真に収めるように指示されることがある。カメラロールにためていく友達も多いが、ノートが自動で分類されたほうがよっぽど楽で便利に決まっている。そこで、Google Driveに板書写真を送信したら自動でその写真を分類するスクリプトを組んでみた。以前の記事の改良版とでも言うべきものだ。
それで、どう改善したの?
僕の塾では、通常授業に加え、夏や冬には夏期講習や冬季講習がある。通常授業は一週間のどの曜日にどの授業があるか決まっているので、曜日に応じてノート画像を分類するだけでよかった。しかし、補習の授業予定に規則性なんて、ない。こんな自分の簡単なロジック、ついに負けたのだ!()ということで、Google Calendarから予定を取得し、それに応じて分類するスクリプトを書いた。
前準備
•IFTTTのアカウント作成
•GoogleCalendarで、授業予定だけを打つカレンダーを作成する、入力しておくそれで、どうやったの?
automaticnote.jsfunction getfolderid(schedule){ console.log("testdata-tweak"); return fldid_reg(getcal()); } function fldid_reg(subject){ //FolderIdの設定 if (subject == "国語"){//Japanese return "[国語ノートのフォルダid]"; } if (subject == "数学"){//math return "[数学ノートフォルダid]"; } if (subject == "科学"){//science return "[科学ノートフォルダid]"; } if (subject == "英語"){//Eng return "[英語ノートフォルダid]"; } if (subject == ""){//undefined return "[教科が不明なときのフォルダid]"; } //exceptions libSlack.computeinfo("captimage-info\n未定義の教科名が検索されました");//(つまりエラー!) return "error"; } function namingservice(){ //日時取得 var date=new Date(); var year=date.getFullYear(); var month=date.getMonth()+1; var day=date.getDate(); var hour=date.getHours(); var minute=date.getMinutes(); var second=date.getSeconds(); //文字列データ生成 var name=year+"-"+month+"-"+day+"-"+hour+"-"+minute+"-"+second+".pdf"; return name; } function getcal(){ var cal = CalendarApp.getCalendarById('[上で作成したカレンダのid]'); var date = new Date(); var events = cal.getEventsForDay(date); var count=0; var date=new Date; var time=date.getTime(); for(;count<events.length;){ var title=events[count].getTitle(); var starttime=events[count].getStartTime(); var endtime=events[count].getEndTime(); if (starttime.getTime()<time && endtime.getTime()>time){ if (title.indexOf("化学")!=-1){return "科学"} if(title.indexOf("数")!=-1){return "数学";} if(title.indexOf("国")!=-1){return "国語";} if(title.indexOf("英")!=-1){return "英語";} if(title.indexOf("物理")!=1){return "科学"} } var count=count+1; } return; } function checktime(event){ } function doPost(e) { try{ //http request var d = e.postData.getDataAsString(); //mainloop var hostfolder=DriveApp.getFolderById("[アップロード先フォルダ]"); var chkd=hostfolder.getFilesByName(d).next(); var scd="regular"; var folderid=getfolderid(scd); var dest=DriveApp.getFolderById(folderid); var name=namingservice(); var t=chkd.makeCopy(name,dest); hostfolder.removeFile(chkd); //slackに通知(こちら側の非公開ライブラリを使ってる) var message="captimage-info\nprocessed data:"+d; libSlack.computeinfo(message); }catch(ex){ var message="captimage-info\nerror\n"+ex.message+"\n"+ex.lineNumber; libSlack.computeinfo(message); } }これを、Google Driveに板書データが追加されるごとにIFTTTで叩く。filenameをPOSTすれば、当該フォルダのそのファイルが分類される。
感想
作ってみると、これまで補習期間には手作業で分類していたノートの管理が楽になった。これからももっと邁進し、自分が手を下さずともことが運ぶものを増やすと面白いかもしれない。
- 投稿日:2019-12-24T02:41:04+09:00
Vue Composition APIでストアパターンをスマートに使って状態管理をする
TL;DR
- [PR] Reactの状態管理ライブラリ「unstated-next」をVue Composition APIベースに移植したよ
- 状態をComposition APIで共有し、Read/Writeできるので便利だよ
- 特定のコンポーネントツリーでしか利用しないようにスコーピングができるよ
- 型もばっちり効くよ
Vue Composition APIのRFCもマージされ、Vue3のalphaもひっそりとリリースされていて、もういくつ寝るとVue3!という雰囲気になってきました。多くのVue.jsユーザがComposition APIの実戦投入について検討をしたり、それに向けた素振りをしているのではないかと思います。今年のアドベントカレンダーでも多くのアドベントカレンダーがComposition APIに触れていたり、いくつかの記事がストアの設計を絡めていて、自分も記事を横にサンプルコードを書いてみたりしました。
ところで、hooksで先行しているReactにはunstated-nextという必要最低限の実装(なんとTSで38行!)でとてもシンプルなライブラリがあります(使い方や仕組みは後述します)。過日業務で使ってみたのですが、導入したエンジニアのアツい推薦も納得するほどの使い勝手の良さでした。
Vueスタックでも同様の状態管理を行えたらなとぼんやりと思っていたのですが、特に同様のライブラリが存在しないようだったのと、Composition APIのドキュメントを読んでいる際に移植が可能だとわかったので作ってみました。
[Github] : https://github.com/resessh/vue-unstated
[npm] : https://www.npmjs.com/package/vue-unstated以下ライブラリの宣伝をしながら解説をする記事になります
![]()
なぜComposition APIだけではだめなのか
※ Composition APIを理解している方は読み飛ばしてください。
Composition APIはそもそもロジックの再利用性を高めることを目的とした仕様です。例えば以下のようなComposition Functionがあるとします。
use/counter.jsexport const useCounter = () => { // 状態 const state = reactive({ count: 0 }) // 状態を変更する関数 const increment = () => { state.count++ } return { state, increment, } }上記のComposition Functionをコンポーネントで使うには以下のように呼び出します。
App.vue<template> <div> <!-- クリックしたらカウントを増やす --> <button @click="increment">+</button> <!-- カウントを表示する --> <p>{{ count }}</p> </div> </template> <script> import { useCounter } from 'use/counter' export default { setup() { // Composition Functionを実行し、初期化された状態とメソッドを取り出す const { state, increment } = useCounter() // templateに状態・メソッドを渡す return { count: state.count, increment, } } } </script>使う際に
useCounter()
でComposition Functionを実行していることから察せられるように、各コンポーネントでuseCounter
を利用しても状態は共有されず、それぞれのコンポーネント内で別々のインスタンスを初期化しているような動作になります。Composition APIは上記コードのとおり、状態とロジックを再利用可能な形で切り出すことがとてもきれいにできる反面、切り出した状態を共有する仕組みは提供されていません。
つまり、このカウンターの状態を共有したい場合は、初期化されたComposition Functionの中身を1何かしらの方法でコンポーネントをまたがって共有しなければなりません。Vue Composition API を使ったストアパターンと TypeScript の組み合わせはどのくらいスケールするか? の記事ではこれを実現するために、 Vueの provide/inject というAPIを使っています。このAPIは「コンポーネントツリーのルートの方で
provide
したものは、同一コンポーネントツリー内であればどんなにルートから遠いコンポーネントでもinject
だけで呼び出すことができる」という機能を利用しています。今回作った
vue-unstated
もこのprovide/inject
を利用していて、基本的に同じ方法・方針で状態管理をしようとしています。デモ
vue-unstatedの使い方
ここからは上記デモのコードを使ってvue-unstatedの使い方について簡単に触れたいと思います。
Composition Functionをつくる
まずはVue Composition APIだけでComposition Functionを作ります。
今回はアイテムを登録することができるだけのTodoListを作ります。use/todos.jsimport { reactive } from "@vue/composition-api" const useTodos = () => { const state = reactive({ items: [], // Todoのアイテムの配列 latestId: 0, // 最新のアイテムのid }) const addItem = item => { state.latestId++ // Todoは { id: number, title: string } の構造 state.items.push({ id: state.latestId, title: item.title }) } return { items: state.items, // 共有したい状態はitemsだけなので、これだけexportする addItem, } }
unstatedコンテナを作る
次に、
vue-unstated
のcreateContainer
でunstatedコンテナにしてexportします。use/todos.js+import { createContainer } from 'vue-unstated' import { reactive } from "@vue/composition-api" const useTodos = () => { const state = reactive({ items: [], // Todoのアイテムの配列 latestId: 0, // 最新のアイテムのid }) const addItem = item => { state.latestId++ // Todoは { id: number, title: string } の構造 state.items.push({ id: state.latestId, title: item.title }) } return { items: state.items, // 共有したい状態はitemsだけなので、これだけexportする addItem, } } +export default createContainer(useTodos)
使いたいコンポーネントの親でprovideする
続けて、使いたいコンポーネントツリーのルートに近いコンポーネント(親側のコンポーネント)でコンテナを
provide
します。App.vue<template> <div id="app"> <todo-register/> <todo-list/> </div> </template> <script> import TodoRegister from "./components/TodoRegister.vue" import TodoList from "./components/TodoList.vue" +import TodoContainer from "./use/todos" export default { name: "App", components: { TodoRegister, TodoList, }, setup() { // const { items, addItem } = TodoContainer.provide() で即座に使うこともできます + TodoContainer.provide() } } </script>
使いたいコンポーネントでuseContainerする
最後に使いたい子コンポーネントでコンテナを
useContainer
します。Todoアイテムのリスト
components/TodoList.vue<template> <ul class="list"> <li v-for="item in items" :key="item.id"> <todo-item :item="item"/> </li> </ul> </template> <script> import TodoItem from "./TodoItem.vue" +import TodoContainer from "../use/todos" export default { name: "TodoList", components: { TodoItem }, setup() { + const { items } = TodoContainer.useContainer() + + return { items } } }; </script>Todoアイテムを登録するフォーム
components/TodoRegister.vue<template> <form @submit.prevent="onSubmit"> <input type="text" v-model="state.title"> <button type="submit">add</button> </form> </template> <script> import { reactive } from "@vue/composition-api" +import TodoContainer from "../use/todos" export default { name: "TodoRegister", setup() { + const { addItem } = TodoContainer.useContainer() const state = reactive({ title: "" }) const onSubmit = () => { + addItem({ title: state.title }) // Todoアイテムの登録 state.title = "" } return { state, onSubmit } } } </script>これだけでTodoアイテムを登録・閲覧できるようになります。
Vue Composition APIを使ってロジックを切り出しただけの状態と比べてもほぼ差がないのがわかるでしょうか?動作の仕組み
本家
unstated-next
も38行と大変短いコードですが、ほぼ同様の実装であるvue-unstated
も34行とわずかなコードで動いています。
基本的な方針は上述の Vue Composition API を使ったストアパターンと TypeScript の組み合わせはどのくらいスケールするか? の 「兄弟コンポーネント間でストアを共有」の項で示されているprovide/inject
でのストアオブジェクトの共有とほぼ同一です。
強いて挙げるとすれば、unstated
ではComposition Functionのインスタンスをまるごと共有しようという発想である点でしょうか?はじめてunstated-next
のコードを読んだ時は、腰が抜けてイスから転げ落ちました。興味がある方はぜひソースを読んでみてください。
https://github.com/resessh/vue-unstated/blob/master/src/index.tsまた、フィーチャーリクエストやコントリビューションもwelcomeですが、どうしてもプロジェクト内の固有の理由で変更を加えたい場合は、プロジェクトのリポジトリにコピペして編集してしまったほうが早いかもしれません2。34行しかないし、アクティブに変更も無いのではないかと思っています。
vue-unstatedを使うことのメリット
vue-unstated
を使うことで得られるメリットがいくつかあるので触れておきます。1. 自分でアノテーションしなくてもバッチリ型がつく
provide/inject
を自分で叩いてストアを共有する場合、inject<HogeStore>(key)
のようにアノテーションでお型付けをする必要があります。
しかし、unstatedコンテナでラップすると、Container.useContainer()
だけでバッチリ型がついたStoreが返ってきます。2. 簡単にstoreの依存関係を表現できる
下記コード例のように、ストア同士の依存関係をシンプルに記述することができます3。
// 検索結果をfetchするだけのComposition const useSearchResult = () => { // ... return { result, search } } export const SearchResultContainer = createContainer(useSearchResult) // 検索結果をフィルタするだけのComposition const useFilteredSearchResult = () => { // 検索結果をfetchするコンテナを利用する const { result } = SearchResultContainer.useContainer(); // フィルタする処理をかける(実際はフィルタのパラメータの状態を持ったりしてもっと複雑になる) const filteredSearchResult = SomeFilterFunction(result); return { result: filteredSearchResult } } export const FilteredSearchResultContainer = createContainer(useFilteredSearchResult)3. 利用しているコンポーネントツリーのインスタンスがGCされた場合、ストアもGCされる。
大規模なアプリケーションを運用していると、主にパフォーマンスの関係で、各ページをDynamic importしたり、いらない状態を消したかったりするのではないでしょうか。
vue-unstated
のメリットというより、provide/inject
のメリットですが、provideしたコンポーネントが消えれば参照カウントでunstatedインスタンスもGCされるため、今表示されているコンポーネントツリーにだけ必要な状態を持つことが可能です。まとめ
Vue Composition APIはまだまだこれからといったフェーズなので、nuxtのサーバサイドプロセスからのhydrationなど、Vuexから離れられなかったり、その他の状態管理方法にそれぞれの必要性や良さがあるのでないかと思っています。
ですが、今回ご紹介したprovide/inject
方式は、いまの所使い勝手の良さからメインストリームになってもおかしくないと思っています。
これから事例を積み上げていってpros/consを精緻にしたり、さらなるベストプラクティスやバッドプラクティスを見つけていけたらなと思っています。
もしライブラリを使ってもらえた場合、気軽にフィードバックいただけるとありがたいです![]()
参考記事
- 投稿日:2019-12-24T01:55:54+09:00
Scratch Extensionをつくってわかったこと
はじめに
皆さんご存知ブロックプログラミングができるScratchですが、いつの間にかExtensionとして自分でブロックを作れるようになったようです 1。色々あってコーディングが苦手な友達の
卒研の手伝いでScratch Extensionを書くことになったので、そこでわかったことをざっとまとめようと思います。Extensionついて、作り始めるための環境づくりや準備なんかについてはいろいろネットにありましたが、その他気になるところについてはドキュメントがあまり見つからなかったので、これが誰かの参考になれば幸いです。
また、自分もjavascriptに関しては素人も同然なので残念なコードや解釈があってもご容赦を。公式ドキュメントと有志のみなさん
Scratch 3.0 Extensions
Scratch Extensionを作るにあたってまず公式ドキュメントは読んでおいたほうが良いです。これを読むことで基本的に必要な要素や構造はわかります。Scratch 3.0の拡張機能を作ってみよう
有志のWikiでも基本的なExtensionの作り方がまとめられています。これらから得られる情報のうち、ここからを読むのに必要な情報をまとめます。
- Extensionとして追加するクラスのgetInfo()関数が返す情報で以下のことを定義する
- 宣言するブロック
- そのブロックの形
- その呼び出し時に実行する関数
- そのブロックに表示される文章
- そのブロックがとる引数
- 引数がつかうプルダウンメニュー
- ブロックに表示される文字列の中で[]で囲んだ文字列は引数として扱われ、中の文字列はプロパティ名になる
- 宣言された引数は、実行される関数の第1引数に含まれる
- プルダウンメニューの内容はリストで書くが、"リストを返す関数の名前"を値とすることで動的なメニューにすることができる
コードや挙動からわかったこと
ここからはドキュメントに書かれていないが、既存のコードや実際の振る舞いを通してわかったことを書きます。
ブロックの実行部分の関数が受け取る引数は1つじゃない
ドキュメントからは「ブロックの処理時に呼ばれる関数が受け取るのは定義された引数たちを持つ第1引数(argsとよく書かれる)だけ」と受け取れますが、引数はそれだけではありません。標準で用意されているブロックのコードを見てみましょう。参照先はcontrolブロックの関数、条件分岐やループのブロックのなかの
< >まで待つ
というフラグを監視して処理をsleepさせるブロックの処理です。scratch-vm/src/blocks/scratch3_control.jswaitUntil (args, util) { const condition = Cast.toBoolean(args.CONDITION); if (!condition) { util.yield(); } }見たとおり2つ目の引数も受け取っていることがわかります。このutilはBlockUtilityクラスのオブジェクトで、「現在自分が実行されているスプライト」や「このブロックは子ブロックかどうか」などの情報を持っています。これをつかわないとIFやLOOPなどの処理を再現することができず、変数の情報なども得られません。
これを見つけるまではひたすらthis.runtimeを掘り進めて時間を無駄にしたのでめんどくさがらずコードを読むことは大切です
LOOPやIFなどとおなじC型ブロックの処理の再現にはutil.startBranch()
ExtensionではblockTypeを
BlockType.CONDITIONAL
やBlockType.LOOP
とすることで既存のIFやLOOPと同じ形のブロックを作ることができます。しかし何も意識せずに実行関数を書いても中にいれたブロックは実行されません。再び標準ブロックのコードを見ます。参照先は同じくcontrolブロックの関数、< >まで繰り返す
といういわゆるwhileを再現したブロックの処理です。scratch-vm/src/blocks/scratch3_control.jsrepeatWhile (args, util) { const condition = Cast.toBoolean(args.CONDITION); // If the condition is true (repeat WHILE), start the branch. if (condition) { util.startBranch(1, true); } }この
util.startBranch()
関数が中のブランチを実行するための関数です。自分でIFやLOOPに近いブロックを作るときはこれを参考にしましょう。おまけにこの関数の定義を見てみます。scratch-vm/src/engine/block-utility.js/** * Start a branch in the current block. * @param {number} branchNum Which branch to step to (i.e., 1, 2). * @param {boolean} isLoop Whether this block is a loop. */ startBranch (branchNum, isLoop) { this.sequencer.stepToBranch(this.thread, branchNum, isLoop); }第1引数のbranchNumは"何番目のブランチを実行するか"です。指定する際のブランチは先頭のものが1、そこから2,3...となります。ブランチの数はブロックのbranchCountというプロパティが参照されます。ここを10にすれば、ブロックを入れる箇所が10箇所のブロックができます。
第2引数のisLoopはループ処理なのかどうかです。ここをtrueにすると、このstartBranch()を呼び出した関数が再度実行されます。ブロックから変数の一覧と値を取得する
普通の?Extensionでは変数の一覧なんて必要ないのかもしれませんが、今回は「複数のリストの要素の組み合せの全パターンのリストをつくるブロック」を作りたかったのでどうしても既存のリストと値を取得する必要がありました。ちなみに「リストのブロック」を受け取る様にすると、要素を全部くっつけた文字列が渡されるので気をつけてください。これも実行時引数utilをつかって実現できます。
const Variable = require('../../engine/variable'); sample (args,util){ console.log(util.target.getAllVariableNamesInScopeByType(Variable.SCALAR_TYPE)) //変数の名前のArray console.log(util.target.getAllVariableNamesInScopeByType(Variable.LIST_TYPE)) //リストの名前のArray console.log(util.target.lookupVariableByNameAndType("hoge",Variable.SCALAR_TYPE)) //名前が"hoge"の変数のオブジェクト console.log(util.target.lookupVariableByNameAndType("fuga",Variable.LIST_TYPE)) //名前"fuga"のリストのオブジェクト }また、今回の様にブロックが使うプルダウンメニューを作るための関数を作る場合は、ブロックから呼び出すわけではないためutilが引数にありません。そんなときはthis.runtimeが持つ関数を使って以下のようにします。基本的には
this.runtime.getEditingTarget()
を使って取得していくのが無難な気がします。const Variable = require('../../engine/variable'); sample_nonBlock (){ const nowSprite = this.runtime.getEditingTarget(); // このブロックがあるスプライト本体 const stage = this.runtime.getTargetForStage(); // デフォルトで存在する「ステージ」のスプライト console.log(nowSprite.getAllVariableNamesInScopeByType(Variable.SCALAR_TYPE)) //グローバル変数とこのスプライトのローカル変数の名前のArray console.log(stage.getAllVariableNamesInScopeByType(Variable.LIST_TYPE)) //グローバルなリストの名前のArray console.log(stage.lookupVariableByNameAndType("hoge",Variable.SCALAR_TYPE)) //名前が"hoge"のグローバル変数のオブジェクト console.log(now_Sprite.lookupVariableByNameAndType("fuga",Variable.LIST_TYPE)) //名前"fuga"のローカルなリストのオブジェクト }
getAllVariableNamesInScopeByType()
やlookupVariableByNameAndType()
はTargetクラスにある関数です。他にも変数のidから変数本体を探すlookupVariableById()
などもあるので変数絡みを触るなら必見です。
getAllVariableNamesInScopeByType()
/lookupVariableByNameAndType()
の定義scratch-vm/src/engine/target.js/** * Look up a variable object by its name and variable type. * Search begins with local variables; then global variables if a local one * was not found. * @param {string} name Name of the variable. * @param {string} type Type of the variable. Defaults to Variable.SCALAR_TYPE. * @param {?bool} skipStage Optional flag to skip checking the stage * @return {?Variable} Variable object if found, or null if not. */ lookupVariableByNameAndType (name, type, skipStage) { if (typeof name !== 'string') return; if (typeof type !== 'string') type = Variable.SCALAR_TYPE; skipStage = skipStage || false; for (const varId in this.variables) { const currVar = this.variables[varId]; if (currVar.name === name && currVar.type === type) { return currVar; } } if (!skipStage && this.runtime && !this.isStage) { const stage = this.runtime.getTargetForStage(); if (stage) { for (const varId in stage.variables) { const currVar = stage.variables[varId]; if (currVar.name === name && currVar.type === type) { return currVar; } } } } return null; }scratch-vm/src/engine/target.js/** * Get the names of all the variables of the given type that are in scope for this target. * For targets that are not the stage, this includes any target-specific * variables as well as any stage variables unless the skipStage flag is true. * For the stage, this is all stage variables. * @param {string} type The variable type to search for; defaults to Variable.SCALAR_TYPE * @param {?bool} skipStage Optional flag to skip the stage. * @return {Array<string>} A list of variable names */ getAllVariableNamesInScopeByType (type, skipStage) { if (typeof type !== 'string') type = Variable.SCALAR_TYPE; skipStage = skipStage || false; const targetVariables = Object.values(this.variables) .filter(v => v.type === type) .map(variable => variable.name); if (skipStage || this.isStage || !this.runtime) { return targetVariables; } const stage = this.runtime.getTargetForStage(); const stageVariables = stage.getAllVariableNamesInScopeByType(type); return targetVariables.concat(stageVariables); }
これらの関数の戻り値はVariableクラスです。このクラスのオブジェクトはhoge.name
にブロックとしての表示名、hoge.value
に変数/リストとしての値を持っています。また、hoge.type
も持っており、これがVariable.LIST_TYPE
だとhoge.value
の値がArrayオブジェクトになります。これを使うことでリストの中身をリストとして取得することができます。変数やリストの値を書き換えたあと反映させる
Targetクラスのおかげで変数やリストを取得し、Variableクラスのオブジェクトの
value
を直接操作することで変数の値が変更できるようになりました。しかし、Scratchには変数やリストの中身を表示するモニターがあります。リストの内容を変更した場合、ただ変更しただけではこのモニターの表示が更新されません。
反映のためには、値を編集したあとVariableクラスのオブジェクトの特定のプロパティを更新する必要があるようです。同じことをしている標準ブロックのコードを見ます。参照先は変数/リストを操作するブロックの関数、<リスト>に()を追加する
というブロックの処理です。scratch-vm/src/blocks/scratch3_data.jsaddToList (args, util) { const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); if (list.value.length < Scratch3DataBlocks.LIST_ITEM_LIMIT) { list.value.push(args.ITEM); list._monitorUpToDate = false; } }ここにある通り、typeが
Variable.LIST_TYPE
なVariableオブジェクトの_monitorUpToDate
をfalse
にすることで変更後の値をモニターに反映することができます。ブロックの引数名が特定の文字列のとき動きがおかしい
→ 名前と値が異なるプルダウンメニューを作れるこれは偶然の産物というか気づきなのですが、blockのargumentsの名前を特定の文字列にすると引数の挙動が変わります。
最初はリストの一覧のメニューの引数の名前をLISTにしていたのがきっかけでした。text: '[LIST]をパターンに追加', arguments: { LIST:{ type: ArgumentType.STRING, menu: 'listMenu' }, }こういうブロックを宣言したところ、普段は
args.TEXT = "文字列"
となるところをargs.LIST = {/*object*/}
が戻りました。
明らかにLIST
という名前の時のみこの現象が起こります。コードを調べに調べた結果、ブロックを実行する際に使われるExecuteクラスにこんな記述を見つけました。scratch-vm/src/engine/execute.js// Store the static fields onto _argValues. for (const fieldName in fields) { if ( fieldName === 'VARIABLE' || fieldName === 'LIST' || fieldName === 'BROADCAST_OPTION' ) { this._argValues[fieldName] = { id: fields[fieldName].id, name: fields[fieldName].value }; } else { this._argValues[fieldName] = fields[fieldName].value; } }どうやら引数の名前が'VARIABLE'or'LIST'or'BROADCAST_OPTION'のときのみ、引数のアイテムがObjectの扱いをされるようです。しかし、プルダウンメニュー本体のMenuのitemはドキュメントによるとstringのArrayのはずなのです。さらに調べるとExtension用のライブラリの一つextension-manager.jsにこんな記述がありました。
scratch-vm/src/extension-support/extension-manager.js/** * Fetch the items for a particular extension menu, providing the target ID for context. * @param {object} extensionObject - the extension object providing the menu. * @param {string} menuItemFunctionName - the name of the menu function to call. * @returns {Array} menu items ready for scratch-blocks. * @private */ _getExtensionMenuItems (extensionObject, menuItemFunctionName) { // Fetch the items appropriate for the target currently being edited. This assumes that menus only // collect items when opened by the user while editing a particular target. const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage(); const editingTargetID = editingTarget ? editingTarget.id : null; const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget); // TODO: Fix this to use dispatch.call when extensions are running in workers. const menuFunc = extensionObject[menuItemFunctionName]; const menuItems = menuFunc.call(extensionObject, editingTargetID).map( item => { item = maybeFormatMessage(item, extensionMessageContext); switch (typeof item) { case 'object': return [ maybeFormatMessage(item.text, extensionMessageContext), item.value ]; case 'string': return [item, item]; default: return item; } }); if (!menuItems || menuItems.length < 1) { throw new Error(`Extension menu returned no items: ${menuItemFunctionName}`); } return menuItems; }なんとMenuのitemにはobjectが許されるようです。しかもObjectの特定のプロパティ、具体的には
text
とvalue
のみが保持されるようです。検証の結果がこれです。上からMenuの項目、名前がLISTなどじゃない引数のときの値、名前がLISTのときの値をそれぞれ出力したものです。
Menuは
item : [ {text: "表示される名前", value: "LISTなど以外で保持される値"}, {text: "表示される名前", value: "LISTなど以外で保持される値"}, ... ]という形で書けば項目名以外の情報を持ったままプルダウンメニューになれることがわかります。また引数の名前が'VARIABLE'or'LIST'or'BROADCAST_OPTION'以外のときはvalueの方の値がargsに入り関数に渡されます。一方名前が'LIST'などのときは
args.LIST
などはオブジェクトになり、args.LIST.id
にvalueが、args.LIST.name
にtextが入ることがわかります。これを使うことことで項目名にある表示される項目名と関数に渡される値が異なるプルダウンメニューを作ることができます。
未だにわからないこと
散々やってもわからなかったことを書きます。
変数やリストの作成後即時反映
Targetクラスを読んだ方はお気づきかもしれませんが、このクラスは
createVariable()
関数を持っています。これは名前の通り変数を作る事ができる関数です。これを内包する形で別のクラスにも変数を作成する関数を持っているものがあります。しかし、実体としての変数やリストは作れるのですが右の全ブロック一覧に反映されません。Runtimeクラスにプロジェクトの変更を知らせるものらしき関数もあるのですが、実行してもとくに変化はありませんでした。
一応右のブロック一覧のタブを変更して戻ってくると反映されます。
変数ブロックから変数自体の名前やidを取得する
ブロックから変数の一覧と値を取得するの冒頭で触れたように、ブロックの引数として変数やリストのブロックを受け取ったときはその値か要素をすべてスペースなしで結合した文字列が渡されます。
ここからどうやってもそのブロックが示す変数やリストに到達できませんでした。標準ブロックが同じことをしていないところから、おそらく仕様上できないのではないかと考えています。終わりに
ScratchのExtension機能はまだβ版らしく、今後本実装に伴って公式非公式問わずドキュメントがもっと充実していくだろうと思います。これはそれまでのつなぎみたいなもんです。もし読んだ方で、「こうやったら変数ブロックから変数の名前とれたよ」とかあったらぜひ教えて下さい。
誰かの卒研に間に合えばとても喜びます。個人的にはGoogleのカード表示以来久々にjavascriptに触りましたが、なれていないからかやっぱり細かいところで引っかかります。型とか...typeofしてもobjectのClassは教えてくれないんですね。しかし今回の1件で、既存のコードを読んで処理内容などを類推するの、謎解きみたいでたのしいなと思えました。またやりたいかというと微妙です。
最後に、javascriptや謎の処理の解読にアドバイスをくれたQiitadonの方々、いつもありがとうございます。毎度助かっています。自分も提供できるほど知識が増えるといいなぁ…
ただし現状ではエクスポート/インポートの機能がないのでローカルに立てたものでのみ使えます。 ↩
- 投稿日:2019-12-24T01:34:58+09:00
Vue.jsとMapbox GL JSでオリジナルの地図を表示してみよう ~Mapbox GL JSの機能を知る編~
はじめに
Vue.jsとMapbox GL JSを使って、このような機能を実装していきます。
- 地名検索
- 国名・地名の日本語化
- 位置情報表示
VueMapbox といったラッパーライブラリもありますが、
Mapbox GL JSの機能を知る編ということで、使わずに進めていきたいと思います。
結果として、Vue.jsの機能は全く使っていませんが・・・
次のステップとして、プラグイン化などしていけたらと思いますmapboxとは
mapboxは、地図を使用したアプリ開発者向けのプラットフォームです。
そのなかでMapbox GL JSは、Web GLを使用して地図を表示するJavaScriptライブラリです。
その他の地図サービス・APIに比べて、カスタマイズ性が高いとされています。環境構築
Vue.jsのプロジェクト作成とMapbox GL JSのインストールを行います。
Vue CLIでプロジェクト作成
vue create mapbox-vue今回はMapbox GL JSの機能を試したいので、
Vue RouterもVuexもなしのシンプルなプロジェクトにしています。? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, TS, Linter ? Use class-style component syntax? Yes ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes ? Pick a linter / formatter config: Prettier ? Pick additional lint features: Lint on save, Lint and fix on commit ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config filesVue CLI Installation
Vue CLI Creating a ProjectMapbox GL JSのインストール
npmでMapbox GL JSをインストール
npm install mapbox-gl @types/mapbox-gl --save※TypeScriptを使用しているため、typesもインストールしています。
/public/index.html
へ以下を追加し、CSSを読み込む/public/index.html(抜粋)<link href="https://api.mapbox.com/mapbox-gl-js/v1.4.1/mapbox-gl.css" rel="stylesheet" />mapboxアカウント作成
Access Tokenを取得するため、
Create your Mapbox accountからアカウントを作成します。
SignUpするとAccountページにAccess Tokenが表示されます。
料金体系は、Webの場合50,000回までのマップ読み込みは無料となっています。
詳細は、Mapbox pricingへ。マップを表示する
まずは、マップを表示してみましょう。
/src/components/MyMap.vue
を追加します。
これからこのファイルを編集して機能を追加していきます。
Your Access Token
の箇所は、先ほど取得したAccess Tokenに変更してください。/src/App.vue<template> <div id="app"> <my-map /> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator' import MyMap from './components/MyMap.vue' @Component({ components: { 'my-map': MyMap } }) export default class App extends Vue {} </script> <style> body { padding: 0; margin: 0; } #app { height: 100vh; } </style>/src/components/MyMap.vue<template> <div id="map"></div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator' import mapboxgl, { MapboxOptions, Map } from 'mapbox-gl' @Component({}) export default class extends Vue { map: Map = {} as Map option: MapboxOptions = { accessToken: 'Your Access Token', container: 'map', style: 'mapbox://styles/mapbox/streets-v8', center: [143.767125, 38.681236], zoom: 4 } mounted() { this.map = new mapboxgl.Map(this.option) } } </script> <style scoped> #map { width: 100%; height: 100%; } </style>仕組み
mapbox-gl
を読み込む/src/components/MyMap.vueimport mapboxgl, { MapboxOptions, Map } from 'mapbox-gl'mapboxのオプションを設定する
/src/components/MyMap.vue(抜粋)option: MapboxOptions = { accessToken: 'Your Access Token', container: 'map', style: 'mapbox://styles/mapbox/streets-v8', center: [143.767125, 38.681236], zoom: 4 }設定できるオプションの詳細は、Mapbox GL JS API reference Map へ。
accessToken
、container
は必須になります。
container
には、マップをバインドするタグのid
を指定します。マップをバインドするタグを作成し、高さを指定する
/src/App.vue(抜粋)<style> body { padding: 0; margin: 0; } #app { height: 100vh; } </style>/src/components/MyMap.vue(抜粋)<template> <div id="map"></div> </template> <style scoped> #map { width: 100%; height: 100%; } </style>Mapオブジェクトを生成する
/src/components/MyMap.vue(抜粋)mounted() { this.map = new mapboxgl.Map(this.option) }DOM作成後である必要があるので、
mounted
フックでMap
オブジェクトを生成します。
このMap
オブジェクトに、マップを操作するファンクションやイベントが定義されています。検索機能を追加する
次に、検索機能を追加してみましょう。
検索機能は、mapbox-gl-geocoder
プラグインから提供されています。
npmで
mapbox-gl-geocoder
をインストールnpm install @mapbox/mapbox-gl-geocoder --save
/public/index.html
へ以下を追加し、CSSを読み込む/public/index.html(抜粋)<link href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-geocoder/v4.4.2/mapbox-gl-geocoder.css" rel="stylesheet" />
Map
オブジェクトにコントロールを追加/src/components/MyMap.vue(抜粋)<script lang="ts"> // 省略 const MapboxGeocoder = require('@mapbox/mapbox-gl-geocoder') @Component({}) export default class extends Vue { // 省略 mounted() { this.map = new mapboxgl.Map(this.option) this.map.addControl( new MapboxGeocoder({ accessToken: this.option.accessToken, mapboxgl: mapboxgl }) ) } } </script>仕組み
検索時には、Geocoding API が呼ばれています。
GET https://api.mapbox.com/geocoding/v5/{endpoint}/{search_text}.jsonMapbox GL JSから、このような
Control
などのUIも提供されていますし、APIとしても提供されていますので、UIを自作することもできます。
その他のAPIの詳細は、mapbox API Documentation へ。地名を日本語化する
英語のままでは見づらいので、日本語化してみましょう。
日本語化は、mapbox-gl-language
プラグインから提供されています。
npmで
mapbox-gl-language
をインストールnpm install @mapbox/mapbox-gl-language --save
Map
オブジェクトにコントロールを追加/src/components/MyMap.vue(抜粋)<script lang="ts"> // 省略 const MapboxLanguage = require('@mapbox/mapbox-gl-language') @Component({}) export default class extends Vue { // 省略 mounted() { this.map = new mapboxgl.Map(this.option) // 省略 this.map.addControl( new MapboxLanguage({ defaultLanguage: 'ja' }) ) } } </script>マップのスタイルを作成する
Mapbox Studioでは、Web上で好きなスタイルを作成することができます。
Mapbox Studio での編集
ここで様々な編集ができます。
詳しい使い方は、Mapbox Studio Manual へ。今回は、日本語のSatellite Streetsを作成しました。
作成したスタイルの適用
option
のstyle
を変更/src/components/MyMap.vue(抜粋)<script lang="ts"> // 省略 @Component({}) export default class extends Vue { // 省略 option: MapboxOptions = { accessToken: 'Your Access Token', container: 'map', style: 'Copied Style URL', center: [143.767125, 38.681236], zoom: 4 } // 省略 } </script>位置情報を表示する
最後に、位置情報を表示してみましょう。
今回は私の好きな美術館を表示してみたいと思います。
レイヤーを追加
/src/components/MyMap.vue(抜粋)<script lang="ts"> // 省略 @Component({}) export default class extends Vue { // 省略 mounted() { // 省略 this.map.on('load', () => { this.map.addLayer({ id: 'points', type: 'symbol', source: { type: 'geojson', data: { type: 'FeatureCollection', features: [ { type: 'Feature', geometry: { type: 'Point', coordinates: [139.775792, 35.715622] }, properties: { title: '国立西洋美術館', icon: 'museum' } }, { type: 'Feature', geometry: { type: 'Point', coordinates: [139.630669, 35.457194] }, properties: { title: '横浜美術館', icon: 'museum' } }, { type: 'Feature', geometry: { type: 'Point', coordinates: [139.051066, 35.2454] }, properties: { title: '彫刻の森美術館', icon: 'museum' } }, { type: 'Feature', geometry: { type: 'Point', coordinates: [139.021225, 35.256709] }, properties: { title: 'ポーラ美術館', icon: 'museum' } }, { type: 'Feature', geometry: { type: 'Point', coordinates: [139.726423, 35.665322] }, properties: { title: '国立新美術館', icon: 'museum' } } ] } }, layout: { 'icon-image': ['concat', ['get', 'icon'], '-15'], 'text-field': ['get', 'title'], 'text-font': ['ヒラギノ角ゴ Pro W3', 'メイリオ', 'sans-serif'], 'text-offset': [0, 0.6], 'text-anchor': 'top' } }) }) } } </script>仕組み
様々なレイヤーを組み合わせることでマップを表示しています。
Layerには主にtype
、sorce
、layout
、paint
が設定できます。
詳しい仕様は、mapbox Style Specification Layers へ。今回は
座標
をアイコン表示
するので、
type
はsymbol
、source
はgeojson
にしています。GeoJSON とは
地理空間を表現するためのJSONフォーマットです。
詳しい仕様は、GeoJSON へ。おわりに
ここまでMapbox GL JSの主要な機能を見てきました。
mapboxのSolutionsには、様々なユースケースが紹介されていますので、こちらもぜひ!業務ではラッパーライブラリを使用していますが、もう少し自由度がほしいと思うこともしばしば・・・。
次は、ここからVue.jsのアプリとしてもっと使いやすくしていきたいと思います
- 投稿日:2019-12-24T01:25:40+09:00
まるで名刺のようなポートフォリオサイトを作れるCardfolio!を公開しました!
こちらの記事はGatsby.js Advent Calendar 2019 24日目の記事です。(メリークリスマスイヴ
)
昨日は@mmnsさんのGatsby, TypeScript, Emotion, Tailwind, MDXでブログを作っているでした!Cardfolio!とは
Cardfolio!とはまるで名刺のようなポートフォリオサイトが作れるGatsbyベースのポートフォリオサイトフレームワークです。
「まるで名刺のようなポートフォリオサイトってなんやねん笑」と思った方、↓のgifを見ていただくとイメージがつくかと思います。そうです!
まるで名刺のようなポートフォリオサイトとは
まるで本物の名刺がそのままWebサイトになったかのような、紙の質感、回転を再現したポートフォリオサイトなのです
(以下カードフォリオサイトと呼びます。)そしてこのサイトが真骨頂を発揮するのは実際に名刺を渡したときにあります!
↓の写真が今回自分が作成した名刺です。
誰かにこんな名刺を受け取ったことを想像しながらQRコードを読み取ってみてください。
(スマホで見ていてQRコード読めねーよという方はこちらからどうぞ)いかがでしょうか?
名刺がそのままポートフォリオに変わったかのような不思議な体験をすることができます。そして今回は誰でも同じようなカードフォリオサイトが作れるようにOSSとしてGithubに公開しています
![]()
ロゴも自分でデザインしてみました〜![]()
カードフォリオサイトの作り方
一応Githubのレポジトリ内にも英語での記載がありますが、今回は日本語向けのカードフォリオサイトを作る方法を簡単にご紹介します。
個人名刺を持ってる or 作りたい方向けかと思いますが、Reactの経験が少しでもある方なら簡単に2-3時間で構築することができますので是非是非作ってみてください![]()
① Githubのレポジトリをフォークします。
② 開発環境を立ち上げます。yarn gatsby developこの時点で試しにlocalhost:8000にアクセスして画面が表示されることを確認してみてください。
③ デフォルトの言語を日本語に切り替えます。
src/data/locales.jsmodule.exports = { ja: { default: true, // enからjaに移動 path: 'ja', locale: 'Japanese', }, en: { path: 'en', locale: 'English', }, }④ 読み込むデータは基本src/data/{locale}.jsonに入っているので、このファイルに必要な情報を入力していきます。
例として先ほどの名刺の表側の名前と職種を変更してみましょう。
以下のようにルートのキーがコンポーネントの名前になっており、その配下に表示するためのデータくる形になっています。src/data/index/ja.json{ "frontSide": { // コンポーネント名 "jobTitle": "デザイナー", // データ "name": "コアラ子" } … }Gatsbyの再ビルドが走り、変更が反映されることが確認できます。
その他のデータについても同様に全て差し替えて行きます。
また、profile.pngのみGatsybyのImgを使用しているため、直接ディレクトリの画像を差し替えてください。
一通り修正が済んだら、localhost:8000にアクセスして反映を確認してみてください。⑤ 完成したらサイトをデプロイします。方法はなんでも良いですが、GatsbyはNetlifyと非常に相性がいいのでおすすめです。
⑥ (オプションですが是非やってほしい) 最後に実際の名刺を作成しましょう!
表面については実装したカードフォリオサイトのデザインに合わせて作成し、
裏面については
こちらのようなQRコードを作成できるサイトで{your-domain}?fromQR=1というurlでQRコードを作成します。
ex) https://matsumotokazuya.io?fromQR=1fromQRはQRコードから遷移したことを識別するためのpropsとして使用します。
(一度名刺を印字すると変更できないので間違えないように注意してください)
作成したQRコードは画像としてqr-code.pngと差し替えるようにしてください。
これであなたの名刺を受け取った方に上記でみていただいたようなびっくり体験をさせることができ、喜ばれること間違いなしですカスタマイズやより詳しい開発の方法はGithubのREADME.mdをご覧ください。
Cardfolio!を支える技術
ここからはQiitaの記事らしくCardfolio!を作るにあたって使用した技術について書いてきます。
名刺の回転
裏表の表示
なんとなく想像がつくかと思いますが、表裏の表示実装は
- 表側と裏側のコンポーネントをそれぞれ作成してcontainer(card)配下にposition: absoluteで重ねて置く
- 裏側を180度回転させる
- Containerを回転させて、表裏を切り替える
という方法で実現しています。
図にすると以下のようになります。1つハマりどころとしてそのまま実装すると裏返ったときに↓のように裏側ではなく表側を反転させたものが表示されてしまいます。
これはtransform3dでDOMを裏返しにした場合、デフォルトではそのオブジェクトの裏側が表示されてしまうためです。
これを解決するためにはそのデフォルトの挙動を変更するbackface-visibilityというマニアックなプロパティをhiddenに設定にする必要があります。.front { ... backface-visibility: hidden; }こうすることで裏返った場合は何も表示されなくなり、結果として裏側が最上部として画面に表示されることになります。
サンプルコードも作成したので参考にどうぞ。ユーザーの操作に合わせて回転させる
ここまでで表示して自動で回転させるところまでは実装することができました。
今度はユーザーのドラッグ操作に合わせてカードを回転させます。
まずドラッグの検知にはreact-use-gestureというライブラリのuseDragというhooksを使って簡単に取得することができます。import { useSpring } from 'react-spring' ... const bind = useDrag(({ down, movement: [moveX], last }) => { ... })ドラッグを検知したら現在のx座標をy軸の角度に変換してカードを回転させます。
// x座標を角度に変換 const degree = lastDegree + moveXToDegree(moveX) ... // ドラッグ中 if (down) { // ユーザーの操作に合わせてカードを回転させる。 set({ transform: `rotateY(${degree}deg)` }) return degree } ...さらに指を離したときに表 or 裏にいい感じに戻ってほしいので、現在の回転角から戻るべき方向を判定して、回転アニメーションをさせながら表 or 裏に戻します。
// ユーザーが指を離したら if (last) { // 裏か表か判定して戻す const horizontalDegree = calcHorizontalDegreeToReturn(degree) setDegree(horizontalDegree) }戻す判定ロジックは以下の図のように
- 第一象限、第三象限にある場合は角度を減らす方向
- 第二象限、第四象限にある場合は角度を増やす方向
に回転させます。
実装の詳細について知りたい方はソースコードをご覧ください。
Gatsby関連
ここまでGatsbyのアドベントカレンダーにも関わらず、Gatsbyの話をほとんどしてこなかったので最後にGatsby関連の技術話を書いておきます
![]()
Gatsby自体がどんなものかはこちらの記事初め他のアドベントカレンダーの記事によくまとまっているので、自分は実装していて悩んだややニッチな話を書きます。fragmentの活用
Cardfolio!は裏側に表示される自己紹介やスキルセットといったモーダルををそれぞれ1つのコンポーネントとして実装しています。
これらのコンポーネントはpropsと取得するGraphQLクエリのデータ構造が一致しているので、それらを1つのコンポーネント内にまとめたいなと考えました。
そこで初めはGraphQLのクエリを変数としてexportして一番上の親コンポーネントから流し込む以下のような実装を試しました。selfIntroduction.tsx// propsの定義 interface Social { name: string id: string url: string } interface Props { data: { description: string menuItemTitle: string socialURLs: Social[] } } const SelfIntroduction = (props: Props) => { ... } ... // propsを取得するためのgraphqlクエリ export const dataQuery = graphql` fragment SelfIntroductionData on IndexJson { selfIntroduction { menuItemTitle description socialURLs { name url } } } ` ...index.tsximport { selfIntroductionQuery } from './selfIntroductionQuery' export default ({ data, location }) => { } export const query = graphql` query Index($locale: String) { file(name: { eq: $locale }, relativeDirectory: { eq: "index" }) { childIndexJson { ${selfIntroductionQuery} ... } } } `これを実行しようとするとビルドの時点でgraphqlタグに変数を埋め込むことができず以下のようなエラーでこけます。
Gatsbyはビルド時に静的にコード解析をしてgraphql``を探し、その中身を実行してコンポーネントに渡すということをしているため外から変数を渡すことができないようです。String interpolation is not allowed in graphql tag: 104 | file(name: { eq: $locale }, relativeDirectory: { eq: "index" }) { 105 | childIndexJson { > 106 | ${selfIntroductionQuery} | ^^^^^^^^^^^^^^^^^^^^^^^^そこでfragmentを使います。
fragmentを使うことで名前の通りクエリを断片として切り出し再利用可能にすることができます。selfIntroduction.tsx// framgmentとしてコンポーネントのpropsを定義 const SelfIntroduction = (props: Props) => { ... } // Framgmentとして必要なデータを取得するクエリを定義 export const dataQuery = graphql` fragment SelfIntroductionData on IndexJson { selfIntroduction { menuItemTitle description socialURLs { name url } } } `利用する側は...{Framgment}の構文でフラグメントを利用できます。
1つのめちゃくちゃ大きなクエリにならずに、fragmentが整然と並んでいてとても読みやすいですねindex.tsxexport const query = graphql` query Index($locale: String) { file(name: { eq: $locale }, relativeDirectory: { eq: "index" }) { childIndexJson { ...SiteMetaData ...FrontSideData ...SelfIntroductionData ...WorksData ...ContactData ...CareerData ...SkillSetData } } } `これでビルド時もエラーにならず、元々やりたかったコンポーネント内にクエリ定義を閉じ込めることができました
![]()
i18n
多言語化するにあたり、今回やりたいこととしてはシンプルに
/ : 日本語ページ
/en : 英語ページのように切り替えたいだけだったので、pluginを使わずにこちらの記事を参考に自前で実装しました。
方法としてはまず以下のようなディレクトリ構成を作り
. ├── index │ ├── en.json │ └── ja.json └── locales.jsen.jsonとja.jsonには実際の言語データを入れておきます。
ja.json{ "frontSide": { "jobTitle": "エンジニア", "name": "ウォンバット太郎" }, ... }en.json{ "frontSide": { "jobTitle": "Engineer", "name": "Wombat John" }, ... }locales.jsには言語の情報を定義しておきます。
そしてgatsby-node.jsでページを生成する際にlocales.jsの情報をもとに各言語のpageを生成します。
また、contextを使いlocaleという変数に言語の情報(ja/en)を渡しておきます。
こうすることでgrapqlのクエリ内で参照することができるようになります。gatsby-node.jsconst locales = require('./src/data/locales') exports.onCreatePage = ({ page, actions }) => { const { createPage, deletePage } = actions return new Promise((resolve) => { deletePage(page) // 各localeのページを生成 Object.keys(locales).map((lang) => { const localizedPath = locales[lang].default ? page.path : locales[lang].path + page.path return createPage({ ...page, path: localizedPath, context: { // contextにlocaleの値を渡しておく locale: lang, }, }) }) resolve() }) }最後にindex.tsx内でcontextのlocaleを使い、データをフェッチする箇所をja/en.jsonを切り替えて取得できるように書き換えます。
export const query = graphql` query Index($locale: String) { file(name: { eq: $locale }, relativeDirectory: { eq: "index" }) { childIndexJson { ...SiteMetaData ...FrontSideData ...SelfIntroductionData ...WorksData ...ContactData ...CareerData ...SkillSetData } } } `これで
/ : 日本語ページ(default)
/en : 英語ページ言語を切り替えたページが生成されるようになりました
![]()
まとめ
いかがだったでしょうか?
Gatsbyは素晴らしいフレームワークで普通にWebサイトを構築するのも良いですが、今回自分が作成したようなOSSをGatsbyベースで作ってみるのもGatsbyの様々な恩恵を受けつつ開発できるのでオススメですこの記事を読んだ方のうち1人でもCardfolio!使って自身のカードフォリオサイトを作ってみていただける方が現れてくれれば嬉しいです!!