20191224のJavaScriptに関する記事は30件です。

年末まで毎日webサイトを作り続ける大学生 〜67日目 オブジェクト指向で�手紙を作る〜

はじめに

こんにちは!@70days_jsです。
今日はオブジェクト指向で手紙を作りました。(gif)↓
test3.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;
}

これで一番上にある手紙はクリックされると消えたように見えます。

感想

うまい具合の文章を考えてくれるプログラムを組んでみたい・・・。
機械学習の方向を勉強するしかないんだろうか・・・?

最後まで読んでいただきありがとうございます。明日も投稿しますのでよろしくお願いします。

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

プロトタイプベースのオブジェクト指向と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
    • プロトタイプベース継承のシンタックスシュガー。
    • クラスベースのオブジェクト指向におけるクラスとは本質的には別物。

流れ

説明の流れとしては、

  1. プロトタイプベースのオブジェクト指向の概念の理解
  2. プロトタイプをふんわりとコードで理解
  3. プロトタイプを実際のコードで理解

という感じで進めます。

プロトタイプベースのオブジェクト指向って?

オブジェクト指向 - wiki
The Early History Of Smalltalk

1, 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 Kay

1, すべてはオブジェクトである。
2, オブジェクトはメッセージの受け答えによってコミュニケーションする。
3, オブジェクトは自身のメモリーを持つ。
4, どのオブジェクトもクラスのインスタンスであり、クラスもまたオブジェクトである。
5, クラスはその全インスタンスの為の共有動作を持つ。インスタンスはプログラムにおけるオブジェクトの形態である。
6, プログラム実行時は、制御は最初のオブジェクトに渡され、残りはそのメッセージとして扱われる。

プロトタイプベース - wiki

全てがクラスベースであるという前提は時に物事を複雑化してしまう。

例えばメソッドが必ず何らかのクラスに所属するという前提は強すぎる場合がある。クラスベースでは委譲や代理(プロキシ)によって動作にバリエーションを与えるが、初めからバリエーションをもったインスタンスを作成できればそのような機構は必要ない。

またインスタンス変数とメソッドの違いとは何か、という問題もある。C++やJavaのpublicなメンバ変数(フィールド)などを別にすれば、インスタンス変数とアクセサメソッドはほとんど等価の概念である。

クラスが存在しない世界ではテンプレート処理によるインスタンス化ができないため、新しいオブジェクトは完全に空の状態か、または他のオブジェクトの複製(クローン)によって作成される。プロトタイプベースでの継承は一般にこのクローンによる特性の引き継ぎを指す。

プロトタイプベースのオブジェクト指向とは、クラスベースのオブジェクト指向とは違ったアプローチでオブジェクト指向を実装したもの。

クラスベースのオブジェクト指向は、クラスという共通の挙動を定義したオブジェクトをテンプレートとして、その挙動を共通に持つ新しいオブジェクトを複数作ることができる。
それに対して、プロトタイプベースのオブジェクト指向にはクラスが存在せず、プロトタイプと呼ばれる共通の挙動を格納したオブジェクトへの参照を持つことで、オブジェクトは共通の挙動を得る。

(この辺は言語によって細かい思想が違うので、一概に同じ言葉で説明出来ない部分もあると思うけど、概念的にはこう)

つまり、どちらにせよ

1, すべてはオブジェクトである。
2, オブジェクトはメッセージの受け答えによってコミュニケーションする。
3, オブジェクトは自身のメモリーを持つ。

これらの思想は受け継いだうえで、オブジェクト間で共通の挙動を、どう定義するかの思想が異なっている。

つまり、プロトタイプベースのオブジェクト指向とは、「複数のオブジェクトに共通の挙動を持たせる方法として、クラスを使用せず、プロトタイプを使用するオブジェクト指向」のこと。

改めてプロトタイプ

したがって、プロトタイプとは、クラスとは違う方法で、複数のオブジェクトに共通の挙動を与える仕組みのこと。

prototypeとは - Weblio

主な意味
原型、模範、原形

プロトタイプと聞くと、「何かを作る際にとりあえず作るもの」というようなイメージが日本語的には強いですが、プロトタイプベースのオブジェクト指向における「プロトタイプ」は「原型」という意味で捉える方がしっくり来ます。「ひな形」「テンプレート」とも言えるでしょう。

オブジェクト指向において、オブジェクトに対して共通の振る舞いを持たせることを「継承」と呼びますが、これはプロトタイプベースであっても同じです。

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から初めて仕様として定義されました。
が、同時に非推奨となっています。
(推奨の方法としてgetPrototypeOfsetPrototypeOfが正式に定義されましたが、このページでは解説のしやすさと理解しやすさから__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__

で確認できます。

スクリーンショット 2019-12-20 22.33.45.png

これは上一部しか表示してませんが、Dateオブジェクトでよく使う関数がオブジェクトの中に入っているのがわかりますよね。上の例で言うcommonFunctionsそのものの参照が取得出来ているのがわかると思います。

さらに、上の例では、全てのオブジェクトに、同じcommonFunctionsオブジェクトの参照を入れてましたよね?
なので、

date1.__proto__ === date2.__proto__ // true
date2.__proto__ === date3.__proto__ // true
date1.__proto__ === date3.__proto__ // true

こうなります。つまり、どのDateオブジェクトも、全て全く同じプロトタイプへの参照(厳密等価演算子===による比較で判断できる)を保持しています。

プロトタイプ本体はどこに?

では、Dateオブジェクトがみんな参照している一つのプロトタイプは、実際どこにあるのでしょうか。
それは、Date関数=Dateコンストラクタが所持しています。

コンストラクタが所持しているプロトタイプは、.prototypeでアクセス出来ます。

スクリーンショット 2019-12-20 22.39.16.png

Date.prototypeによって得られたオブジェクトは、date1.__proto__で得られたオブジェクトと全く同じです。

// 厳密等価演算子でtrueが返る
Date.prototype === date1.__proto__ // true

つまりjsでは、オブジェクトの共通の挙動として、自身のコンストラクタのプロトタイプを参照します。

プロトタイプチェーン

さて、プロトタイプは発見できたので、プロトタイプ上にある関数を実行してみましょう。

date1.getFullYear(); // 2019

2019が返ってきました。
でもこれ、よく考えると変ですよね?
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

スクリーンショット 2019-12-20 23.27.33.png

これこれ!

じゃあ、逆に言えば、date1オブジェクトにgetFullYearプロパティがあったら、そっち返すっていうことだよね?

const date1 = new Date();
date1.getFullYear = 123; // 問題なく通る

結果は...

date1.getFullYear(); // 123

123が返ってきました。
先ほど書いたプロトタイプチェーンの仕様通りですね。

しかし、ここまでは「チェーン」って言うほどチェーンしてません。
ということでここから、継承の話に移ります。

継承

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にはクラスは存在しませんが、コンストラクタは存在します。

コンストラクタ関数の使用 - MDN

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によるオブジェクト作成は何が違うんでしょうか。

new 演算子 - MDN

  1. 空の JavaScript オブジェクトを生成する
  2. このオブジェクト (のコンストラクター) を他のオブジェクトへリンクする
  3. ステップ 1 で新しく生成されたオブジェクトを this コンテキストとして渡す
  4. 関数が自分自身を返さない場合は this を返す

注目すべきは

  1. このオブジェクト (のコンストラクター) を他のオブジェクトへリンクする

です。
ここだけ曖昧な表現になっていてよくわかりませんが、具体的に意識するべきなのは以下の2点です。

  1. 新規作成したオブジェクトのconstructorプロパティにコンストラクタ関数自体の参照を代入する。
  2. 新規作成したオブジェクトの[[prototype]]にコンストラクタ関数のプロトタイプの参照を代入する。

jsのES5までの仕様では、インスタンスオブジェクトのプロトタイプへの直接の書き込み権限がプログラマにありません([[prototype]]がプライベートであり、かつ__proto__は仕様に存在しなかったため)。

つまり、newを使ってのみ、プロトタイプ継承を用いたオブジェクト生成をすることができます。

jsにおけるnewとは、継承されたオブジェクトを作成するベストな方法であると言えるでしょう。

class構文とプロトタイプ継承

TODO: 同じものを書く場合の比較とかを頑張って書く

オートボクシング

TODO: プリミティブ値とプロトタイプの話を頑張って書く

プロトタイプ汚染

TODO: 頑張って書く

おしまい

ということで、現状まだ書いてないことがいっぱいあるのですが、プロトタイプの最低限を書いたので一旦終わりとします。
続きはまたどこかで頑張って書きます。

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

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);
//true

indexOfメソッドも似たような役割ですが、こちらは指定した値が含まれる場合は要素のインデックスを、ない場合は−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);
}


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

誰でも小説家になれるWebアプリ

IT芸人 Advent Calendar 2019 25日目の記事です。

前置き

皆様おはようございます。DE-TEIUです。
今回はIT芸人の記事ということで、ちょっとしたWebアプリを開発しました。
こいつどの記事でも結局やってる事同じとか言うな

成果物

無限の猿定理」という言葉をご存知でしょうか。
これは、「文字列をランダムに並べ続けると、どんな文字列でもいつかは見つかるはずである」という定理の事です。
猿が適当にキーボードをガチャガチャやっていればいつかは文学作品を書き上げる、というような表現が名前の由来になっているようです。

参考:無限の猿定理 - Wikipedia

とはいえ、時間は有限なので、文学作品を書き上げたいからといって本当に完成するまで適当にキーボードを叩くわけにもいきません。

と、いうわけで、誰でも手っ取り早く文学作品を書き上げられるWebアプリを開発しました。

スーパー作家ー

xab7i-h7ong.gif

文学史に残る名著が

書けたと思います。

これを使えば自分でキーボードを叩くか、とかげやねこがキーボードの上を歩くだけでも作品が出来上がります。
書いている最中はきっと無我夢中で書いていて内容が頭に入ってこないと思われますので、書き上げてからじっくり読みましょう。

解説

概要

このアプリは、静的サイトを公開するWebサーバーと、青空文庫の著作権切れの作品をランダムに取得するためのAPIサーバーの2つで構成されています。

Untitled Diagram (1).png

  • 静的サイトを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アプリの開発に向いてそうですね。

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

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);
    });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Moment.jsを使って年齢計算

サンプル

sample.js
import 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.js
import 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());
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6でjqueryアニメーションライブラリanimsitionの使用 | 躓いたことなど...

1. rails6でanimsitionを使う

ビューにアニメーションを付けようと思い、試しにanimsitionというjqueryライブラリを使った.いくつか、躓いて勉強になったことをまとめる.

2. まずはダウンロード

ライブラリを下記のサイトからダウンロード(ZIP形式)
https://github.com/blivesta/animsition

3. webpacker経由だと動かない...

★解凍後、以下の2ファイルををapp/javascript/srcにコピー.
①dist/js/animsition.js
②dist/js/animsition.css
★app/javascript/packs/application.jsに追記

application.js
import "../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.rb
Rails.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

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

これからはFunction Componentですべて解決できる――というのはどうやら幻想だったようです。

何がしたかったのか

Reactには、Lazy Componentというものがあります。

MyComponent.tsx
import React, { FC } from 'react';

const MyComponent: FC = () => (
  <div>Hello LazyComponent!</div>
);

export default MyComponent;
MyApp.tsx
import 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.js
import 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;

React 16.6の新機能、React.lazyとReact.Suspense を使った非同期処理

こう書いたら、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>
  );
}

あれ、動かん:thinking::thinking::thinking:
動かんぞ。

useStateに置き換える前は動いたコードが、置き換えた瞬間動かなくなりました。てゆうか、setterは普通に呼ばれているはずなのに、stateの値は0のまま。なんでや、、、。

諦めてComponent classにしてみた

というわけで、PromiseTestの実装をComponent classに変えてみました。statethis.state.statesetterthis.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>
  );
}

こうすると、動きました。動いてしまいました。え、なんでなんや、、、:thinking:
……Function Componentが使えない極めてまれなケースの1つを発見した身としては、非常に頭が痛いです。こういう重要なことはもっとわかりやすくドキュメントに書いておいて下せぇ……。

結論

React、なんもわからん。

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

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.js
import { 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.js
export 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 でした。

おわります。
メリークリスマス??

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

Quill EditorでHTML5をそのまま挿入する方法

ブラウザ

スクリーンショット 2019-12-24 19.48.49.png

説明

個人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 += "[![N|Solid](https://cldup.com/dTxpPi9lDf.thumb.png)](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>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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に代入したと言う事になります。

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

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使うとかも一案だと思った

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

もう一度理解する、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種類に分けることができます。
それは、プリミティブオブジェクトです。

プリミティブとは、数値・文字列・真偽値・シンボル・nullundefinedのことを指します。3
オブジェクトとはプリミティブではないものすべてで、基本的にはObjectとそれを継承したもののことです。4

配列は上に挙げたプリミティブのどれにも当てはまりませんから、配列はオブジェクトであるということになります。
具体的には、Arrayオブジェクトのインスタンスのことを配列といいます。
実際、typeof array"object"となり、array instanceof ObjectArray instanceof Objecttrueです。

もう少しだけ詳細な話をします。記事の本題からやや逸れるので、興味のない方は次の節まで読み飛ばしてください。
配列は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なものとして、たとえば文字列やSetMapが挙げられます。

また、普通に配列を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

ここでyukarinyukari_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]

これらの方法を使うことで、コピー先の配列に手を加えたとしても元の配列は変化しないようにできますね。

余談ですが、これはsliceconcat非破壊的なメソッドであることを利用してコピーを行う方法です。
反対の意味として、sortpushのような破壊的なメソッドが挙げられます。
破壊的なメソッドはオブジェクト(配列)に直接手を加えて変化させるもので、非破壊的なメソッドは、新しいオブジェクトを生成するなどして元のオブジェクトは変化させないものを指します。

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()を呼び出すことができなくなってしまいます。

ExArrayArrayを継承している以上配列といえる気がするので、これを正しくコピーできるというのは重要ですね。
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]); // false

2019/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
深いコピーを行う場合には、専用の処理を自前で実装する必要があります。

ただし、深いコピーを行うとき、それが本当に必要なのかという点については留意しておくべきでしょう。
二次元配列のようなシンプルな場合を除き、配列が入れ子になった構造が生じている時点で、それらを木構造を持ったオブジェクトなどの形で表現してしまったほうがよいことも十分に考えられます。
また、配列を何でもかんでもコピーしてしまうと、変更したと思った要素が変更されておらず、逆にバグを生み出してしまうことがあるかもしれません。

配列のコピーについての基礎的な事項をしっかり理解して、思わぬバグを防ぐようにしていきましょう。


  1. 以後、arrayと書いたときには、[][1,2,3]などの任意の配列を指すこととします。 

  2. よくわからない、という方も、後でもう一度説明をしますので安心してください。 

  3. https://www.ecma-international.org/ecma-262/10.0/index.html#sec-primitive-value また、近いうちにStage 4のBigIntが新しいプリミティブとして追加されると思われます。 

  4. 例外として、Objectを継承しないオブジェクトとしてObject.create(null)で生成したものなどがあります。 

  5. https://www.ecma-international.org/ecma-262/10.0/index.html#sec-property-accessors 

  6. https://www.ecma-international.org/ecma-262/10.0/index.html#sec-object-type 

  7. https://www.ecma-international.org/ecma-262/10.0/index.html#sec-property-accessors-runtime-semantics-evaluation 

  8. もしかしたらIterableIteratorはECMAScriptの用語ではなく、TypeScriptで型付けに使われている用語だったかもしれません。 

  9. もしかしたら $1$ から数え始める場合もあるかもしれません。とりあえずこの記事では $0$ から始めています。 

  10. 逆に、あらゆる変数が非破壊的であるといろいろなメリットがあるので、オブジェクトの不変性をより重視して破壊的メソッドの使用を嫌う場合もあります。 (https://immutable-js.github.io/immutable-js/

  11. https://www.ecma-international.org/ecma-262/10.0/index.html#sec-array.from 

  12. https://www.ecma-international.org/ecma-262/10.0/index.html#sec-tointeger 

  13. プロトタイピングで手っ取り早くバグを埋め込むのはやめろ つまり使うな 

  14. 厳密には無限ループで止まらなくなるのではなく、再帰が深くなりすぎてMaximum call stack size exceededエラーが発生して停止します。 

  15. そもそもみんな配列を継承して新しいクラスを作ったりしませんよね なので基本的には[...array]だけ覚えておけば充分です。 

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

関数とは[JavaScript本格入門]__WebデザイナーのJS学習備忘録

はじめに

Qiita3回目の投稿になります。今回は学習のJavaScriptの基本構文「関数とは」についてまとめていきます。

JavaScriptの人気書籍「JavaScript本格入門」を学習教材として使用しているのですが、かなり分厚い書籍で持ち歩くのも不便なのでいつでも見れるように内容を凝縮してQiitaに投稿して共有できればと思い、投稿することにしました。

※ この記事の内容は基本的な関数の構文に関する説明のため、私と同じようにJavaScript入門者に向けての記事となります。

関数とは

与えられたパラメーター(引数)に基づいてあらかじめ決められた処理を行い、その処理の結果を返す仕組みを関数という。
JSではデフォルトで多くの関数を提供しているが、自分で関数を定義することもできる。自分で定義した関数のことをデフォルトの関数と区別してユーザー定義関数という。

ユーザー定義関数を定義するには、4つの方法がある。

1. function命令で定義する
2. Functionコンストラクター経由で定義する
3. 関数リテラル表現で定義する
4. アロー関数で定義する

function命令で定義する

● function命令 - 構文 -

script.js
function 関数名(引数, ...) {
  実行する処理
  return 戻り値;
}

関数名を付ける時の注意点

  • (単なる文字列ではなく)識別子の条件を満たす必要がある
  • 「その関数がどのような処理を実行するのか」すぐ把握できる名前を付ける

→「動詞+名詞」の形式で命名するのが一般的
→camelCase形式
名前の先頭が小文字、単語の区切りを大文字で表す記法のことで関数名はcamelCase記法で表すのが基本

引数

引数は関数の挙動を決めるためのパラメーターである。
呼び出し元から指定された値を受け取るための変数を、カンマ区切りで指定する。
仮引数ともいい、関数内部でのみ参照できる。

戻り値(返り値)

関数が処理の結果、最終的に呼び出し元に返すための値のことである。
通常、関数の末尾にreturn命令を記述して指定する。
関数の途中でreturn命令を記述したら、それ以降の処理は実行されない。

戻り値がない(返す値がない)- 呼び出し元に値を返さない関数では、return命令を省略しても構わない。
return命令が省略された場合、関数はデフォルトでundefined(未定義)を返す。

関数の定義

▼ サンプルコード

script.js
function getTriangle(base, height) {
  return base * height / 2;
}

console.log("三角形の面積:" getTriangle(5,2));    //結果:5

定義済みの関数を呼び出す方法

script.js
関数名([引数,...]);

関数定義で宣言された引数(仮引数)と区別する意味で、呼び出し側の引数を実引数という。

関数の注意点

  • 関数の後方の丸カッコ()は省略できない。
  • 丸カッコを省略した場合、関数の定義内容がそのまま出力されてしまう。

Functionコンストラクター経由で定義する

JavaScriptには組み込みオブジェクトとしてFunctionオブジェクトを用意しており、関数はこのFunctionオブジェクトのコンストラクターを利用して定義することもできる。

● Functionコンストラクター - 構文 -

script.js
var 変数名 = new Function(引数,... , 関数の本体);

▼ サンプルコード

script.js
var getTriangle = new Function('base','height', 'return base * height / 2');
console.log("三角形の面積:" + getTriangle(5,2));    //結果:5

Functionコンストラクターには、関数が受け取る仮引数を順に並べ、最後に関数の本体を指定するのが基本

Functionコンストラクターには省略する記述方法がある

new演算子を省略してあたかもグローバル関数であるかのように記述することもできる

script.js
var getTriangle = Function('base','height','return base * height / 2');



仮引数の部分を1つの引数として記述することもできる

script.js
var getTriangle = new Function('base, height', 'return base * height / 2');

Functionコンストラクターを使用するメリット

実は、特別な理由がない限り、あえてFunctionコンストラクターを利用するメリットはない。

だが、Functionコンストラクターはfunction命令にはない重要な特徴として「Functionコンストラクターでは、引数や関数本体を文字列として定義できる」という点がある。

▼ サンプルコード

script.js
var 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.js
var getTriangle = function(base, height) {
  return base * height / 2;
};
console.log('三角形の面積:' + getTriangle(5,2));    //結果:5

function命令と関数リテラルの違い

function命令 → 関数getTriangleを直接定義している。
関数リテラル → 「function(base, height){...}」と名前のない関数を定義した上で、変数getTriangleに格納している。

関数リテラルは宣言した時点で、名前を持たないことから匿名関数または、無名関数と呼ばれることもある。

アロー関数で定義する

アロー関数はES6(ES2015)で新たに追加された記法で、関数リテラルをよりシンプルに記述できる。

● アロー関数 - 構文 -

(引数,...) => {...関数の本体...}

▼ サンプルコード

script.js
let getTriangle = (base, height) => {
  return base * height / 2;
};

console.log('三角形の面積:' + getTriangle(5,2));    //結果:5

アロー関数では、functionを書かず、代わりに=>で引数と関数本体をつなぐ。

アロー関数はこの他にコードをシンプルに記述する方法がある。

本体が一文である場合には、ブロックを表す{...}を省略できる
文の戻り値がそのまま戻り値と見なされるので、return命令も省略可能。

script.js
let getTriangle = (base, height) => base * height / 2;



引数が一個の場合には、引数をくくるカッコも省略可能

script.js
let 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つになります。
それぞれの関数の定義の方法だけでなく、その違いや特徴、どのユーザー定義関数で関数を定義するべきなのか理解を深めていく必要があると実感しています。

今回は基本的な内容について解説してきましたが、今後も学習した内容をより詳しくまとめていこうと思います。

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

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 として読み込む

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

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.jsbuildModules の箇所に設定を追加します。

nuxt.config.js
export 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.diff
  build: {
-    extend(config, ctx) {}
+    extend(config: any, ctx: any) {}
  }

nuxt.config.ts に下記を追加します。

nuxt.config.ts
typescript: {
  typeCheck: true,
  ignoreNotFoundWarnings: true
}

eslint-config-typescriptをインストール

npm install --save-dev @nuxtjs/eslint-config-typescript

続いて、 package.jsonscriptlint を修正します。

package.json.diff
  "scripts": {
-    "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
+    "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ."
  }

eslintrc.jsextends に下記を追加します。
@nuxtjs がある場合は削除します。

eslintrc.js.diff
  extends: [
-   '@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.ts
declare 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.jsoncompilerOptions"skipLibCheck": true を追加します。

tsconfig.json
"compilerOptions": {
  "skipLibCheck": true,
}

nuxt-property-decoratorをインストール

クラスを使用するために、nuxt-property-decoratorをインストールします。

npm install --save-dev nuxt-property-decorator

nuxt-property-decoratorを有効にするために、 tsconfig.jsoncompilerOptions"experimentalDecorators": true を追加します。

tsconfig.json
"compilerOptions": {
  "experimentalDecorators": true,
}

TypeScriptを使ってみる

pages/index.vuescript の箇所を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.jsbabel-eslint を削除します。

eslintrc.js
parserOptions: {
  parser: 'babel-eslint' // この行を削除
}

確認

http://localhost:3000/にアクセスして、下記の様な画面が確認できれば完了です!
お疲れ様でした!

nuxt-ts-sample

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

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 コード数の少なさに改めて驚く。

トップ

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

ネイティブの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>

と書きます。

これで下ごしらえは完了です。

本題です

画面収録 2019-12-24 11.14.33.mov.gif

今回は、<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.js
var 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ライフを楽しみましょう!

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

SvelteでSwipe Card UIを作ったので、所感をまとめます

この記事はJavaScript 2 Advent Calendar 2019、24日目の記事です。

Svelteを使って、ブラウザで動作するSwipe Card UIを作りました。
スマホにしか対応していませんが、こちらにデプロイしてあるので、興味がある方は触ってみてください。

Dec-24-2019 09-10-59.gif

(ソースコードはgithubで公開しています)

はじめに:Svelteとは

  • Write less code
  • No virtual DOM
  • Truly reactive

の三拍子をアピールポイントにしている、Javascriptフレームワーク(正確にはコンパイラ)です。
最新バージョンはv3。
「React,Vue,Svelte」という立ち位置を目指しています。
日本語の記事を色々探るより、
公式のHPと、作者が登壇した際の動画を見るのが一番分かりやすくて詳しいと思います。

Svelteのコードの書き方の基本については、Write less code
に出てくるサンプルコードを見れば、ひと目わかると思います。
画像になりますが、貼っておきます。

スクリーンショット 2019-12-24 9.01.25.png

コード量はたしかに少ないですね。
(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} 
  />

ここで設定しているswipestartswipemoveは、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を用いて
ネイティブイベントが呼び出された時に特定のCustomEventdispatchEventで発火させています。
関数の返り値として、removeEventLitenerが設定されているので、余計なイベントの監視はありません。

CustomEventdispatchEventは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プロパティで設定することが多かった
durationeasingの計算をJS内で完結することができます。

Springでは、変化の「stiffiness(かたさ)」や「damping(跳ね返り)」も含めた定義が可能です。
Swipe Card UIには、Springをピンポイントで使用しています。
特に便利だったのは、タップをやめてカードが元の位置に戻る際のdampingです。

Dec-24-2019 09-11-30.gif

これらの機能が行うのは変数の変化までなので、その値をUIと結びつける必要があるのですが、
今回はインラインstyle内で変数を使用して結びつけました。

おわりに

今回使った機能の他にも、アプリ内でグローバルに値を管理するためのStore
propsの受け渡しをなくすためのContext APIなど、Svelteのコードにはいくつも特徴的な点があります。
公式のチュートリアルによくまとまっているので、
気になる方は触ってみてください!

個人的に、
Vueが好きな人はとっつきやすいかもしれませんが、
Reactが好きな方にはあまり好かれなさそうだな、と思いました。

ReactやVueと何がいいんだ、という点についての詳細は、
画面遷移や実際にアプリとして使用できる機能を持つwebアプリを作った時に振り返ります。
(サンプルケースだけで比較している記事はよくあるのですが、それだけで実務で通用するか否かは分からないと思っている)

1月中に作れるかな!
進捗は多分、twitterでつぶやくと思います。

ちなみに:コンパイル後の容量

Svelteは、コンパイル後のコードにはvanillaJSしか含まれず、軽量なことをアピールしています。
今回実際に作成したファイルの容量は・・・

スクリーンショット 2019-12-24 9.14.35.png

25KB!
確かに軽い。

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

Babel Plugin を作りながら AST と Babel を学ぶ

この記事は JavaScript Advent Calendar 2019 の 23日目の記事です。
前日の22日目は Vue-CLI 4を使用したJavaScript開発環境構築(プロトタイプ版とプロジェクト版) でした。

今回は表題通りBabel Pluginを作りながらASTBabelを学ぼうという記事です。

AST とは?

まずは根幹であるASTについて軽く説明します。
ASTAbstract Syntax Treeの略で、日本語では抽象構文木などと呼ばれるものです。

ASTはプログラムの構造を示したデータ構造体であり、JavaScriptではJSONデータの形で表現されることが一般的になり、基本的に仕様は、ESTree に準拠されています。

ASTBabel以外に ES Lintwebpack などにも使用されています。

実際にASTがどのようなものなのかをAST explorerというサイトで簡単に確認することができます。
今回はconst a = 1ASTの構造体にしてみました。

画面左側が実際の値、右側がASTの構造体になります。
これは acorn というミニマムな JavaScript の parser により生成されたものです。

スクリーンショット 2019-12-24 8.07.17.png

こちらがそのデータになります。

{
  "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 です。

元々Babel6 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
スクリーンショット 2019-12-24 8.25.37.png

イメージとしてはこのようになります。
スクリーンショット 2019-12-24 8.27.14.png

Babel はコードをどのように変換するのか?

Babelの変換にはこのように3つの段階があります。

  • Parsing
  • Transformation

  • Code Generat

図にするとこのようになります。
スクリーンショット 2019-12-24 8.32.30.png

実際に Babel Plugin を作りながら AST を学ぶ

ここからは実際にBabel Pluginを作りながらBabelがどのようにASTを駆使してコードを変換しているのか見ていきましょう。
今回はconstletをすべて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

ここから本題であるconstletをすべて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の仕様に相当するEsTreeVariableDeclarationを検索してみましょう。
検索結果はこちら

このような検索結果コードが表示されています。

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;

これで、constletvarに変更することができました。

ほかにもこのようにすることで値の変更もできます。

// 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の肝となる部分になります。
ASTBabel Pluginの肝を理解すれば、 @babel/plugin-transform-arrow-functions のコード のようなプラグインがやっていることは案外簡単なことのように思えるかもしれません。

ASTを使って便利ツールを作っていきましょう!!

今回使用したソースコードはこちらのレポジトリにあります。
https://github.com/sakito21/babel-plugin-demo

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

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-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ヘッダーの確認が必要です。配信するサーバの環境に合わせ確認してください。 image.png
    • 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'
    }),
  });

ブラウザで確認

  • chromeで地データが反映されているか確認します。丹沢のあたりですが、地形データは上手く読み込めてそうです。

    image.png

  • 丹沢付近から御殿場方面にかけて確認してみると、地形データが関東エリア周辺に絞ったため、未反映の部分が目立ちます。
    image.png

以上になります。

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

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.js
var 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: メディアをプリロードすべきか指定する、nonemetadataautoのいずれかの値をとる
  • 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.js
document.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

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

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.js
document.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.js
self.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側から自分自身を終了する。

参考

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

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

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

画像ファイルが選択されたらブラウザ上でリサイズして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でサーバサイドに送ることが多い模様)

参考

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

GASで「イレギュラーな予定に対応できる」そんなノート分類システム

What I wanted to do?

僕の塾では、以前記事で書いたとおり、塾のノートを一度写真に収めるように指示されることがある。カメラロールにためていく友達も多いが、ノートが自動で分類されたほうがよっぽど楽で便利に決まっている。そこで、Google Driveに板書写真を送信したら自動でその写真を分類するスクリプトを組んでみた。以前の記事の改良版とでも言うべきものだ。

それで、どう改善したの?

僕の塾では、通常授業に加え、夏や冬には夏期講習や冬季講習がある。通常授業は一週間のどの曜日にどの授業があるか決まっているので、曜日に応じてノート画像を分類するだけでよかった。しかし、補習の授業予定に規則性なんて、ない。こんな自分の簡単なロジック、ついに負けたのだ!()ということで、Google Calendarから予定を取得し、それに応じて分類するスクリプトを書いた。

前準備

•IFTTTのアカウント作成
•GoogleCalendarで、授業予定だけを打つカレンダーを作成する、入力しておく

それで、どうやったの?

automaticnote.js
function 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すれば、当該フォルダのそのファイルが分類される。

感想

作ってみると、これまで補習期間には手作業で分類していたノートの管理が楽になった。これからももっと邁進し、自分が手を下さずともことが運ぶものを増やすと面白いかもしれない。

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

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

以下ライブラリの宣伝をしながら解説をする記事になります :pray:

なぜComposition APIだけではだめなのか

※ Composition APIを理解している方は読み飛ばしてください。

Composition APIはそもそもロジックの再利用性を高めることを目的とした仕様です。例えば以下のようなComposition Functionがあるとします。

use/counter.js
export 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 を利用していて、基本的に同じ方法・方針で状態管理をしようとしています

デモ

実際にコードを見たり触ったりできるデモはこちらになります。
Edit [vue-unstated DEMO] Todo

vue-unstatedの使い方

ここからは上記デモのコードを使ってvue-unstatedの使い方について簡単に触れたいと思います。

:one: Composition Functionをつくる

まずはVue Composition APIだけでComposition Functionを作ります。
今回はアイテムを登録することができるだけのTodoListを作ります。

use/todos.js
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,
  }
}

:two: unstatedコンテナを作る

次に、 vue-unstatedcreateContainer で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)

:three: 使いたいコンポーネントの親で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>

:four: 使いたいコンポーネントで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を精緻にしたり、さらなるベストプラクティスやバッドプラクティスを見つけていけたらなと思っています。
もしライブラリを使ってもらえた場合、気軽にフィードバックいただけるとありがたいです :pray:


参考記事


  1. この場合 useCounter() で得られた { state, increment } を指す。 

  2. unstated-next を利用していたときは、react-routerとの兼ね合いで、コードを変更せざるを得なかったことがあった気がします。 

  3. まだ実際に動かして試してないので、動かなかったらメンゴです。動くように直します。 

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

Scratch Extensionをつくってわかったこと

はじめに

皆さんご存知ブロックプログラミングができるScratchですが、いつの間にかExtensionとして自分でブロックを作れるようになったようです 1。色々あってコーディングが苦手な友達の卒研の手伝いでScratch Extensionを書くことになったので、そこでわかったことをざっとまとめようと思います。Extensionついて、作り始めるための環境づくりや準備なんかについてはいろいろネットにありましたが、その他気になるところについてはドキュメントがあまり見つからなかったので、これが誰かの参考になれば幸いです。
また、自分もjavascriptに関しては素人も同然なので残念なコードや解釈があってもご容赦を。

公式ドキュメントと有志のみなさん

  • Scratch 3.0 Extensions
    Scratch Extensionを作るにあたってまず公式ドキュメントは読んでおいたほうが良いです。これを読むことで基本的に必要な要素や構造はわかります。

  • Scratch 3.0の拡張機能を作ってみよう
    有志のWikiでも基本的なExtensionの作り方がまとめられています。

これらから得られる情報のうち、ここからを読むのに必要な情報をまとめます。

  • Extensionとして追加するクラスのgetInfo()関数が返す情報で以下のことを定義する
    • 宣言するブロック
      • そのブロックの形
      • その呼び出し時に実行する関数
      • そのブロックに表示される文章
      • そのブロックがとる引数
    • 引数がつかうプルダウンメニュー
  • ブロックに表示される文字列の中で[]で囲んだ文字列は引数として扱われ、中の文字列はプロパティ名になる
  • 宣言された引数は、実行される関数の第1引数に含まれる

引数関連のサンプル
//こんなブロックを宣言したら
//前略
opcode: 'supaFunc',
blockType: BlockType.COMMAND,
text: '[TEXT]を追加', // <- TEXTのところが入力欄になる
arguments: {
    TEXT: {
        type: ArgumentType.STRING,
        defaultValue: 'ことば'
    },
//中略
supaFunc(arg){
    console.log(arg.TEXT); //ことばor記入された文字列
}

上のように書くとこんな見た目のブロックができる。

  • プルダウンメニューの内容はリストで書くが、"リストを返す関数の名前"を値とすることで動的なメニューにすることができる

コードや挙動からわかったこと

ここからはドキュメントに書かれていないが、既存のコードや実際の振る舞いを通してわかったことを書きます。

ブロックの実行部分の関数が受け取る引数は1つじゃない

ドキュメントからは「ブロックの処理時に呼ばれる関数が受け取るのは定義された引数たちを持つ第1引数(argsとよく書かれる)だけ」と受け取れますが、引数はそれだけではありません。標準で用意されているブロックのコードを見てみましょう。参照先はcontrolブロックの関数、条件分岐やループのブロックのなかの< >まで待つというフラグを監視して処理をsleepさせるブロックの処理です。

scratch-vm/src/blocks/scratch3_control.js
    waitUntil (args, util) {
        const condition = Cast.toBoolean(args.CONDITION);
        if (!condition) {
            util.yield();
        }
    }

出典 : scratch-vm scratch3_control.js l99-104

見たとおり2つ目の引数も受け取っていることがわかります。このutilはBlockUtilityクラスのオブジェクトで、「現在自分が実行されているスプライト」や「このブロックは子ブロックかどうか」などの情報を持っています。これをつかわないとIFやLOOPなどの処理を再現することができず、変数の情報なども得られません。

これを見つけるまではひたすらthis.runtimeを掘り進めて時間を無駄にしたのでめんどくさがらずコードを読むことは大切です

LOOPやIFなどとおなじC型ブロックの処理の再現にはutil.startBranch()

ExtensionではblockTypeをBlockType.CONDITIONALBlockType.LOOPとすることで既存のIFやLOOPと同じ形のブロックを作ることができます。しかし何も意識せずに実行関数を書いても中にいれたブロックは実行されません。再び標準ブロックのコードを見ます。参照先は同じくcontrolブロックの関数、< >まで繰り返すといういわゆるwhileを再現したブロックの処理です。

scratch-vm/src/blocks/scratch3_control.js
    repeatWhile (args, util) {
        const condition = Cast.toBoolean(args.CONDITION);
        // If the condition is true (repeat WHILE), start the branch.
        if (condition) {
            util.startBranch(1, true);
        }
    }

出典 : scratch-vm scratch3_control.js l118-125

この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);
    }

出典 : scratch-vm block-utility.js l99-104

第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 target.js l201-235

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);
    }

出典 : scratch-vm target.js l479-500


これらの関数の戻り値はVariableクラスです。このクラスのオブジェクトはhoge.nameにブロックとしての表示名、hoge.valueに変数/リストとしての値を持っています。また、hoge.typeも持っており、これがVariable.LIST_TYPEだとhoge.valueの値がArrayオブジェクトになります。これを使うことでリストの中身をリストとして取得することができます。

変数やリストの値を書き換えたあと反映させる

Targetクラスのおかげで変数やリストを取得し、Variableクラスのオブジェクトのvalueを直接操作することで変数の値が変更できるようになりました。しかし、Scratchには変数やリストの中身を表示するモニターがあります。リストの内容を変更した場合、ただ変更しただけではこのモニターの表示が更新されません。
スクリーンショット 2019-12-23 23.46.59.png
反映のためには、値を編集したあとVariableクラスのオブジェクトの特定のプロパティを更新する必要があるようです。同じことをしている標準ブロックのコードを見ます。参照先は変数/リストを操作するブロックの関数、<リスト>に()を追加するというブロックの処理です。

scratch-vm/src/blocks/scratch3_data.js
    addToList (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;
        }
    }

出典 : scratch-vm scratch3_data.js l126-133

ここにある通り、typeがVariable.LIST_TYPEVariableオブジェクトの_monitorUpToDatefalseにすることで変更後の値をモニターに反映することができます。

ブロックの引数名が特定の文字列のとき動きがおかしい
→ 名前と値が異なるプルダウンメニューを作れる

これは偶然の産物というか気づきなのですが、blockのargumentsの名前を特定の文字列にすると引数の挙動が変わります。
最初はリストの一覧のメニューの引数の名前をLISTにしていたのがきっかけでした。

            text: '[LIST]をパターンに追加',
            arguments: {
                LIST:{
                    type: ArgumentType.STRING,
                    menu: 'listMenu'
                },
            }

こういうブロックを宣言したところ、普段はargs.TEXT = "文字列"となるところをargs.LIST = {/*object*/}が戻りました。
スクリーンショット 2019-12-24 0.17.30.png
明らかに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;
            }
        }

出典 : scratch-vm execute.js l305-319

どうやら引数の名前が'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;
    }

出典 : scratch-vm extension-manager.js l335-371

なんとMenuのitemにはobjectが許されるようです。しかもObjectの特定のプロパティ、具体的にはtextvalueのみが保持されるようです。

検証の結果がこれです。上からMenuの項目、名前がLISTなどじゃない引数のときの値、名前がLISTのときの値をそれぞれ出力したものです。
スクリーンショット 2019-12-24 1.00.16.png

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クラスにプロジェクトの変更を知らせるものらしき関数もあるのですが、実行してもとくに変化はありませんでした。
一応右のブロック一覧のタブを変更して戻ってくると反映されます。

タブ移動で変数が出現する様子
ezgif.com-resize.gif

変数ブロックから変数自体の名前やidを取得する

ブロックから変数の一覧と値を取得するの冒頭で触れたように、ブロックの引数として変数やリストのブロックを受け取ったときはその値か要素をすべてスペースなしで結合した文字列が渡されます。
スクリーンショット 2019-12-24 1.41.54.png
ここからどうやってもそのブロックが示す変数やリストに到達できませんでした。標準ブロックが同じことをしていないところから、おそらく仕様上できないのではないかと考えています。

終わりに

ScratchのExtension機能はまだβ版らしく、今後本実装に伴って公式非公式問わずドキュメントがもっと充実していくだろうと思います。これはそれまでのつなぎみたいなもんです。もし読んだ方で、「こうやったら変数ブロックから変数の名前とれたよ」とかあったらぜひ教えて下さい。誰かの卒研に間に合えばとても喜びます。

個人的にはGoogleのカード表示以来久々にjavascriptに触りましたが、なれていないからかやっぱり細かいところで引っかかります。型とか...typeofしてもobjectのClassは教えてくれないんですね。しかし今回の1件で、既存のコードを読んで処理内容などを類推するの、謎解きみたいでたのしいなと思えました。またやりたいかというと微妙です。
最後に、javascriptや謎の処理の解読にアドバイスをくれたQiitadonの方々、いつもありがとうございます。毎度助かっています。自分も提供できるほど知識が増えるといいなぁ…


  1. ただし現状ではエクスポート/インポートの機能がないのでローカルに立てたものでのみ使えます。 

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

Vue.jsとMapbox GL JSでオリジナルの地図を表示してみよう ~Mapbox GL JSの機能を知る編~

はじめに

Vue.jsとMapbox GL JSを使って、このような機能を実装していきます。

  • 地名検索
  • 国名・地名の日本語化
  • 位置情報表示

VueMapbox といったラッパーライブラリもありますが、
Mapbox GL JSの機能を知る編ということで、使わずに進めていきたいと思います。
結果として、Vue.jsの機能は全く使っていませんが・・・
次のステップとして、プラグイン化などしていけたらと思います:bow_tone1:

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 files

Vue CLI Installation
Vue CLI Creating a Project

Mapbox GL JSのインストール

  1. npmでMapbox GL JSをインストール

    npm install mapbox-gl @types/mapbox-gl --save
    

    ※TypeScriptを使用しているため、typesもインストールしています。

  2. /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 GL JS Install

mapboxアカウント作成

Access Tokenを取得するため、
Create your Mapbox accountからアカウントを作成します。
SignUpするとAccountページにAccess Tokenが表示されます。
image.png

料金体系は、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>

すると、このように地図が表示されます!
image.png

仕組み

  1. mapbox-gl を読み込む

    /src/components/MyMap.vue
    import mapboxgl, { MapboxOptions, Map } from 'mapbox-gl'
    
  2. 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 へ。
    accessTokencontainerは必須になります。
    containerには、マップをバインドするタグのidを指定します。

  3. マップをバインドするタグを作成し、高さを指定する

    /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>
    
  4. Mapオブジェクトを生成する

    /src/components/MyMap.vue(抜粋)
    mounted() {
      this.map = new mapboxgl.Map(this.option)
    }
    

    DOM作成後である必要があるので、mountedフックでMapオブジェクトを生成します。
    このMapオブジェクトに、マップを操作するファンクションやイベントが定義されています。

検索機能を追加する

次に、検索機能を追加してみましょう。
検索機能は、mapbox-gl-geocoder プラグインから提供されています。

  1. npmで mapbox-gl-geocoder をインストール

    npm install @mapbox/mapbox-gl-geocoder --save
    
  2. /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" />
    

    GitHub mapbox-gl-geocoder

  3. 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>
    

すると、このように検索ができるようになります。
mapbox検索.gif

仕組み

検索時には、Geocoding API が呼ばれています。

GET https://api.mapbox.com/geocoding/v5/{endpoint}/{search_text}.json

Mapbox GL JSから、このような Control などのUIも提供されていますし、APIとしても提供されていますので、UIを自作することもできます。
その他のAPIの詳細は、mapbox API Documentation へ。

地名を日本語化する

英語のままでは見づらいので、日本語化してみましょう。
日本語化は、mapbox-gl-language プラグインから提供されています。

  1. npmでmapbox-gl-languageをインストール

    npm install @mapbox/mapbox-gl-language --save
    

    GitHub mapbox-gl-language

  2. 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>
    

すると、このように国名や地名が日本語になります。
image.png

マップのスタイルを作成する

Mapbox Studioでは、Web上で好きなスタイルを作成することができます。

Mapbox Studio での編集

  1. New styleをクリック
    image.png

  2. 好きなテンプレートを選択し、Customize xxxをクリック
    image.png

  3. エディターが表示される
    image.png

ここで様々な編集ができます。
詳しい使い方は、Mapbox Studio Manual へ。

今回は、日本語のSatellite Streetsを作成しました。

作成したスタイルの適用

  1. シェアアイコンをクリック
    Mapbox Studio share.png

  2. Style URLをコピー
    Mapbox Studio style.png

  3. optionstyleを変更

    /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>
    

すると、作成したスタイルが適用されます。
image.png

位置情報を表示する

最後に、位置情報を表示してみましょう。
今回は私の好きな美術館を表示してみたいと思います。

  1. レイヤーを追加

    /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>
    

すると、このようにアイコンと地名が表示されます。
image.png

仕組み

様々なレイヤーを組み合わせることでマップを表示しています。
Layerには主に typesorcelayoutpaint が設定できます。
詳しい仕様は、mapbox Style Specification Layers へ。

今回は 座標アイコン表示 するので、
typesymbolsourcegeojson にしています。

GeoJSON とは

地理空間を表現するためのJSONフォーマットです。
詳しい仕様は、GeoJSON へ。

おわりに

ここまでMapbox GL JSの主要な機能を見てきました。
mapboxのSolutionsには、様々なユースケースが紹介されていますので、こちらもぜひ!

業務ではラッパーライブラリを使用していますが、もう少し自由度がほしいと思うこともしばしば・・・。
次は、ここからVue.jsのアプリとしてもっと使いやすくしていきたいと思います:bow_tone1:

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

まるで名刺のようなポートフォリオサイトを作れるCardfolio!を公開しました!

こちらの記事はGatsby.js Advent Calendar 2019 24日目の記事です。(メリークリスマスイヴ :santa_tone1: )
昨日は@mmnsさんのGatsby, TypeScript, Emotion, Tailwind, MDXでブログを作っているでした!

Cardfolio!とは

アセット 7@2x.png
Cardfolio!とはまるで名刺のようなポートフォリオサイトが作れるGatsbyベースのポートフォリオサイトフレームワークです。
「まるで名刺のようなポートフォリオサイトってなんやねん笑」と思った方、↓のgifを見ていただくとイメージがつくかと思います。

cardfolio.gif

そうです!
まるで名刺のようなポートフォリオサイトとは
まるで本物の名刺がそのままWebサイトになったかのような、紙の質感、回転を再現したポートフォリオサイトなのです :card_box:
(以下カードフォリオサイトと呼びます。)

そしてこのサイトが真骨頂を発揮するのは実際に名刺を渡したときにあります!
↓の写真が今回自分が作成した名刺です。
誰かにこんな名刺を受け取ったことを想像しながらQRコードを読み取ってみてください。
(スマホで見ていてQRコード読めねーよという方はこちらからどうぞ)

cardfolio.png

いかがでしょうか?
名刺がそのままポートフォリオに変わったかのような不思議な体験をすることができます。:smiling_imp:

そして今回は誰でも同じようなカードフォリオサイトが作れるようにOSSとしてGithubに公開しています :octocat:
ロゴも自分でデザインしてみました〜:paintbrush:

Cardfolio!
アセット 7@2x.png

カードフォリオサイトの作り方

一応Githubのレポジトリ内にも英語での記載がありますが、今回は日本語向けのカードフォリオサイトを作る方法を簡単にご紹介します。
個人名刺を持ってる or 作りたい方向けかと思いますが、Reactの経験が少しでもある方なら簡単に2-3時間で構築することができますので是非是非作ってみてください :pray:

① Githubのレポジトリをフォークします。
② 開発環境を立ち上げます。

yarn
gatsby develop

この時点で試しにlocalhost:8000にアクセスして画面が表示されることを確認してみてください。

③ デフォルトの言語を日本語に切り替えます。

src/data/locales.js
module.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の再ビルドが走り、変更が反映されることが確認できます。
front.png

その他のデータについても同様に全て差し替えて行きます。

また、profile.pngのみGatsybyのImgを使用しているため、直接ディレクトリの画像を差し替えてください。
一通り修正が済んだら、localhost:8000にアクセスして反映を確認してみてください。

⑤ 完成したらサイトをデプロイします。方法はなんでも良いですが、GatsbyはNetlifyと非常に相性がいいのでおすすめです。

⑥ (オプションですが是非やってほしい) 最後に実際の名刺を作成しましょう!
表面については実装したカードフォリオサイトのデザインに合わせて作成し、
裏面については
こちらのようなQRコードを作成できるサイト

{your-domain}?fromQR=1

というurlでQRコードを作成します。
ex) https://matsumotokazuya.io?fromQR=1

fromQRはQRコードから遷移したことを識別するためのpropsとして使用します。
(:warning: 一度名刺を印字すると変更できないので間違えないように注意してください)

作成したQRコードは画像としてqr-code.pngと差し替えるようにしてください。
これであなたの名刺を受け取った方に上記でみていただいたようなびっくり体験をさせることができ、喜ばれること間違いなしです :boar:

カスタマイズやより詳しい開発の方法はGithubのREADME.mdをご覧ください。

Cardfolio!を支える技術

ここからはQiitaの記事らしくCardfolio!を作るにあたって使用した技術について書いてきます。

名刺の回転

裏表の表示

なんとなく想像がつくかと思いますが、表裏の表示実装は

  • 表側と裏側のコンポーネントをそれぞれ作成してcontainer(card)配下にposition: absoluteで重ねて置く
  • 裏側を180度回転させる
  • Containerを回転させて、表裏を切り替える

という方法で実現しています。
図にすると以下のようになります。

グループ 8.png

1つハマりどころとしてそのまま実装すると裏返ったときに↓のように裏側ではなく表側を反転させたものが表示されてしまいます。
Cursor_と_awesome-poitras-dnk0s_-_CodeSandbox.png

これは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)
    }

戻す判定ロジックは以下の図のように

  • 第一象限、第三象限にある場合は角度を減らす方向
  • 第二象限、第四象限にある場合は角度を増やす方向

に回転させます。

judge.png

実装の詳細について知りたい方はソースコードをご覧ください。

Gatsby関連

ここまでGatsbyのアドベントカレンダーにも関わらず、Gatsbyの話をほとんどしてこなかったので最後にGatsby関連の技術話を書いておきます :bow:
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.tsx
import { 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が整然と並んでいてとても読みやすいですね :thumbsup:

index.tsx
export const query = graphql`
  query Index($locale: String) {
    file(name: { eq: $locale }, relativeDirectory: { eq: "index" }) {
      childIndexJson {
        ...SiteMetaData
        ...FrontSideData
        ...SelfIntroductionData
        ...WorksData
        ...ContactData
        ...CareerData
        ...SkillSetData
      }
    }
  }
`

これでビルド時もエラーにならず、元々やりたかったコンポーネント内にクエリ定義を閉じ込めることができました :clap:

i18n

多言語化するにあたり、今回やりたいこととしてはシンプルに

/ : 日本語ページ
/en : 英語ページ

のように切り替えたいだけだったので、pluginを使わずにこちらの記事を参考に自前で実装しました。

方法としてはまず以下のようなディレクトリ構成を作り

.
├── index
│   ├── en.json
│   └── ja.json
└── locales.js

en.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.js
const 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 : 英語ページ

言語を切り替えたページが生成されるようになりました :earth_africa:

まとめ

いかがだったでしょうか?
Gatsbyは素晴らしいフレームワークで普通にWebサイトを構築するのも良いですが、今回自分が作成したようなOSSをGatsbyベースで作ってみるのもGatsbyの様々な恩恵を受けつつ開発できるのでオススメです :gift:

この記事を読んだ方のうち1人でもCardfolio!使って自身のカードフォリオサイトを作ってみていただける方が現れてくれれば嬉しいです!!

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