TypeScript入門以前ガイド

某社で自分が React/Redux + TypeScript などの講習をやってみた結果、TypeScript 入門用資料が必要だと思って書いたやつです。

このドキュメントのターゲット

  • TypeScript で書かれたプロジェクトに参加する人
  • TypeScript を導入するために、その事前知識が必要な人

自分が React/Redux などの講習でいろいろやってみた結果、 ES2015 と TypeScript を同時に教えると、初学者は何がどの概念に由来するかの区別が出来ずに混乱します。なので、ES5 -> ES2015, ES2015 -> TypeScript にわけて解説することにします。

先に言っておくと、 TypeScript 固有の機能というのはほぼ存在せず(ほぼ enum のみ)、ES2015 の個々の文法に対して、型アノテーションの文法がちょっとずつ拡張されているだけです。

このドキュメントの読み方

前提: プログラミングの基本がわかること(四則演算、変数、関数などの概念)

  • JavaScript を詳しく知らない => ES2015 for Beginners 編へ
  • ES5 + jQuery の人 => ES2015 for ES5 Programmers 編へ
  • ES2015 はわかるが、 import/export がわからない => ES Modules 編へ
  • ES2015 + ES Modules がわかる => TypeScript 編へ
  • 実運用を知りたい人 => エコシステム編へ

最初に ES2015 の解説をして、次に TypeScript がそれをどう拡張するか説明します。

ES2015 for Beginners

あなたはプログラマーとして JavaScript を初めて学ぶ人間です。プログラミングの基本的な概念は知っています。もしかしたら JavaScript を少しいじったことがあるかもしれませんが、大きなアプリケーションを書いたことはありません。

この言語は複雑怪奇な(残念な)歴史を持っており、いろいろな都合で謎の慣習があったりなかったりしますが、新しく学ぶ人は ES2015 と名乗る仕様だけを追えばオッケーです。むしろ、2015 年より古い情報は、目を通さない方が良いぐらいです。

今の JavaScript は、ES2015, ES2016, ES2017... と毎年言語の仕様が更新されます。ES2015 は特別なバージョンで、このリリースサイクルになる基点で、かつ大規模な機能追加が行われたバージョンだからです。ES とは EcmaScript の略で、ブラウザによる拡張などを除いた、純粋な言語機能だけを指します。

IE 以外のブラウザ(Chrome, Safari, Edge, Firefox)は、いずれも 2 ヶ月から 6 ヶ月の間隔で更新され、どのブラウザも ES2015 と呼ばれる水準なら、基本的にカバーしています。

しかし、現実には 2018 年においてはまだ IE をサポートしたい、するべきという意見が多数派でしょう。 そのため、JavaScript の開発者コミュニティにおいては、ES2015 で書いたコードを ES5 に変換して配布するのが主流です。IE のサポートが切れる 2021~2024 年まで、この状況は続くことでしょう。

この記事ですべての文法を解説しませんが、日本語でのES2015の入門は、 JavaScript の入門書 #jsprimer を推奨しています。

知っておくとよい雰囲気

  • 基本的な文法自体は,比較的シンプルな C/Java の系譜
  • 関数が number や object のようにファーストクラスオブジェクトであり、つまり変数に代入可能。関数参照なので、オーバーロード不可
  • 歴史的経緯によって Java から多数の予約語を借用しているが、Java との互換性はない
  • 歴史的経緯と後方互換性によって、非推奨な機能が多々残っている。具体的には with(...) {...}, var による変数宣言, == による比較など。
  • 関数型を目指した時期(ブレンダン・アイクによる初期実装)、Java を目指した時期(廃止された ES4 と ActionScript との分岐)、Python に影響を受けた時期(Mozilla による JavaScript 1.x)、CoffeeScript 経由で Ruby に影響を受けた時期(Arrow Functon, class), TypeScript 経由で C# に影響を受けている時期(最近)がある
  • 使う人によって、書かれるコードの雰囲気がかなり異なる

ES2015 for ES5 Programmers

あなたは一昔前の JavaScript で生きてきた人間です。 jQueryAPI は一通り知っています。サーバーで生成した HTML にクラスをつけて $(...)セレクタを捕まえて、その中身を書き換えてきました。$.map(...) による制御や、 var self = this のようにクロージャの this を保存して再利用してきたかもしれません。

ES2015 入門するにあたっては、ES5 までに覚えた「お約束」は、全部忘れてください。

  • var による変数宣言は推奨されなくなりました。再代入しない値は const、再代入を許可する変数は let を使ってください。ほとんど const でいいはずです。 const - JavaScript | MDN
  • window.App = {...} のようなグローバル変数によるモジュール渡しは行いません。トップレベルスコープでの代入も暗黙のグローバル変数への推奨されません(onload = function() {...})。ファイルスコープと、 import/export による参照渡しによって行います(ES Modules 編を参照)
  • jQuery の DOM 操作は、昔はブラウザ間の差異を吸収する役割がありましたが、今では標準 API で十分で、そちらが推奨されています。たとえば $(".js-items") ではなく、document.querySelectorAll(".js-items") を使ってください。 You Might Not Need jQuery
  • var self = this は、 arrow function によってほぼ不要になりました。これは、function() {...}() => {...} と書ける機能なのですが、この関数スコープ内の this は親スコープの this を引き継ぎます。 アロー関数 - JavaScript | MDN
  • ES2015 の ES Modules 環境においては、トップレベルの thisundefined です。 window ではありません。
  • 非同期表現は jQuery.Deferred ではなく Promise を使ってください。Promise による API が利用可能ならば async/await を使ってください。jQuery3 以降の jQuery.Deferred は Promise と互換になりましたが、レガシー環境では jQuery 2 以上の採用は稀です。 Promise を使う - JavaScript | MDN
  • 複雑な prototype 継承は(表向きには)行わなくました。 class 宣言 を使ってください。内部的には prototype 継承イディオムと同じことが行われています。 クラス - JavaScript | MDN
  • ネットワーク通信は jQuery.ajax ではなく、window.fetch が推奨されます。ただし、fetch 自体が冗長な使い方を必要になるケースが多く、post を多用する場合は axios の方がよく使われます。いずれも Promise ベースの API です。 Fetch 概説 - Web API インターフェイス | MDN axios/axios: Promise based HTTP client for the browser and node.js

ES Modules

ES Modules(略称 ESM) は JavaScript に他の言語のようなモジュールシステムを導入する機能です。

import - JavaScript | MDN

import/export によって、外部のファイルパスを指定することで、export されたオブジェクトを呼ぶことが出来ます。また、ファイルごとにファイルスコープを持ちます。(僕が知ってる言語の中では python の module system が一番似てます)

// src/a.js
import b, { c } from './b.js'
b();
console.log(c);

// src/b.js
const v = 1; // 外から見えない

// default は特殊化されています
export default () => {...};
export const c = v;

依存が静的に決定されるので、グローバル変数同士で依存がある場合と違って、初期化順の問題が発生しません。(循環参照だと未解決のundefinedになることはある) IE 以外のモダンブラウザでは、<script type="module" src="main.js"></script> という風に呼ぶと、 ESM でコードを書くことが出来ます。

ただし、機能として実装されているだけで、実際にはあるファイルをエントリーにした時、「非同期なファイル取得 => パース => 依存決定 => 非同期なファイル取得 => パース => ...」という処理を繰り返してしまうので、お世辞にも最適化されてるとは言えません。依存を静的に解析して配信サーバーを最適化するための仕様がまだ安定していないためです。(将来的にHTTP/2 Push をベースに読み込みを最適化する仕組みが考えられています)

IE で動かないこと、読み込みの最適化が行われないこと + npm のモジュールを依存に含みたいことが多い、といった理由で、Webpack というツールで、一つのファイルをエントリーにして、一つのファイルに固めてしまうことが現状ベストプラクティスとされています。

webpack

おまけ: CommonJS について

2011~ に書かれたコードと、NodeJS のコードは module.exports = ...require('...') による ESM ではないモジュールシステムを見ることがあります。これらはファイルシステム相対パスでの参照解決という点では ESM と似ていて、実際に ESM の仕様策定に大いに影響があったのですが、これを browserify というツールでクライアントでも node.js のエミュレーションをする、というツールが流行った結果、これをモジュールシステムとして採用するフレームワークが増えました。

node.js では未だに ESM の扱いが experimental なので、「モジュールシステム以外は ES2015」というライブラリも比較的よく見かけます。

今盛んに使われている webpack も初期バージョンでは ESM を一旦 commonjs に変換して解決していましたが、今のバージョンでは import を直接解釈して結合しています。

Webpack によって、ESM と commonjs は相互読み出しが可能になっているのですが、 commonjs では default に相当する概念がないので、扱いがやや特殊になっています。commonjs から export default {} を参照したい場合 require('./foo').defaultという形になります。ただし、Webpack による特殊化であって、標準化された振る舞いではありません。

非同期表現: Promise と async/await

(やや難しいので必要になるまで読まなくて良い)

PromiseJavaScript で非同期処理を表現するための機能です。

「1 秒待つ関数」を表現するのは次のようになります。

const wait = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(), 1000);
  });

wait().then(() => console.log("wait done"));

reject(...) を呼ぶと失敗処理になります

const willFail = () =>
  new Promise((resolve, reject) => {
    reject(new Error("reason..."));
  });

willFail()
  .then(() => console.log("never"))
  .catch(() => console.log("come here"));

ES2017 では、これを構文的に表現する async/await という文法が追加されました。

const main = async () => {
  await wait();
  console.log("after 1000");
  try {
    await willFail();
  } catch (e) {
    console.log("come here");
  }
};

main();

内部的には Promise(と正確には ES2015 で追加された ジェネレーター関数) の糖衣構文です。

async で宣言された関数の中では、 await で Promise の解決を待ちます。 await の中での非同期例外は 例外機構 try {...} catch(error) {...} で表現されます。 また、この関数の返り値は必ず Promise のインスタンスになります。関数が終了すると resolve される Promise オブジェクトです。

トップレベルスコープでの await は今現在、仕様で許可されていませんが、将来的には可能になる予定です。Chrome の DevTools では例外的に許可されています。

基本的には、非同期関数は Promise で表現しつつ、使う側は async/await で解決する、という形になるでしょう。

TypeScript

TypeScript の型は、基本的には JavaC# と似ています。Javaの影響を受けた C# の作者 Anders Hejlsberg が設計した JavaScript 拡張です。

入門資料

TypeScript in 5 minutes · TypeScript

Basic Types · TypeScript

typescript-ninja/typescript-in-definitelyland: TypeScript in Definitelyland ちょっと古い

バージョンごとの変更点は vvakame さんの Qiita が参考になります 「user:vvakame tag:typescript」の検索結果 - Qiita

TypeScript は C#よりもかなり柔軟な型の表現を持ちます。これはもともと動的な JavaScript の表現を可能なかぎりカバーするために、豊富な表現が可能になっています。

  • SubType
  • Union Type
  • Generics
  • 値型(いわゆる存在型とはちょっと違います)

TypeScript コンパイラの機能は、次の 3 つに分類することが出来ます

  • 静的な型のチェッカー
  • 型の除去
  • ES2015 から ES5 への変換

TypeScript ユーザーの知るべきこととして、TypeScript の型は、JS への変換時にはただ取り除かれるだけです。型によってランタイムの処理が変わることはありません。

// before
const x: number = 1;
class C implements Base<K> {
  private foo(): void {
    return;
  }
}
// after
const x = 1;
class C {
  foo() {
    return;
  }
}

とはいっても、ランタイムへの影響が出る例外が 2 つあります。

  • 非標準な enum 機能。実際に値を生成するので、ランタイムに関与します。
  • 非標準な namespace 機能。ES Modules が策定されるより前の機能なので、基本的に使う必要はありません。

標準ライブラリや外部ライブラリなどは ジェネリクスとともに表現されることがあります。

// promise の例
async function fetchFoo(): Promise<string> {
   const res = await fetch('/api/foo')
   return res.text()
}

// react の例
import React from 'react'
export default class MyApp extends React.Component<{a: number}, {b: number}> {
  render() {
    return <div>app</div>
  }
}

エコシステム編

という章を頑張って書こうと思いましたが、もうこのあとは試行錯誤なので、いくつかポインタを置いておくだけにします。

チーム開発でやる場合、社内の強い人を呼んでくるとか、僕みたいなフリーランス呼んでください。誰かが一度書けば、だいたい1年はメンテナンスフリーで回ります。 運用編、あとで書くかも。

off-the-main-thread の時代

off-the-main-thread は今フロントエンドで熱いテーマの一つです。日本語圏では今ひとつ話題になってないので紹介しておきます。

off-the-main-thread の概念の大まかな概要については、Chrome 開発者の nhiroki さんの日本語の記事があるので、こちらを参照してください。

nhiroki.jp

speakerdeck.com

ここまでのあらすじ

従来のウェブブラウザーでは、一つの画面につき一つ割り当てられる、UI スレッドと呼ばれる名前空間で様々な処理を行ってきました。DOMセマンティクスの評価, CSS による rendering / painting、JSのScripting…。もちろん裏側ではブラウザが様々なバックグラウンドサービスに処理を委譲し、スレッドで実行され、その非同期な結果を受け取っているわけですが、少なくともUIスレッドで走るJSからコントロールできる範囲ではシングルスレッドです。

このような状況で、最近のフロントエンドの重厚長大化は、CPUの専有という形で、ユーザーの体験の悪化につながっている、と Chrome デベロッパーの Alex Russell は指摘しています。

The "Developer Experience" Bait-and-Switch | Infrequently Noted

僕個人の意見としては、フロントエンド開発者にとっては拡充される機能郡は一つの福音でありつつも、現実にはIEというブラウザに律速に縛られています。その結果、将来的なゴールは示されているが、現実が追いついていない、ならポリフィルとしての層(babel や webpack)が分厚くなるのは当然だという気がします。

havelog.ayumusato.com

とはいえ、HTML5 と PWA という2つのムーブメントで、ブラウザの機能的な不足は段階的に解消されてきました。また将来に渡って改善していくためのディスカッションの場も、先の十年よりも遥かに増えており、リリースサイクルも主要なブラウザはほぼローリングリリースを採用しており(Safari の不透明感はありつつも)そこまで悲観するものではない、という状況です。実際には、短命なモバイルデバイスのライフサイクルに助けられている形ですが…。あとは、他にEOLなIEが死ぬのを待つだけ。

blog.jxck.io

フロントエンドのリッチ化は ー ときに批判にさらされつつも ー 機能的な不足は解消されつつあり、代わりにパフォーマンスの問題に向き合う時期が来た、というのが自分の認識です。

どう向き合うか

メインスレッドが専有されるとどういう問題が起きるでしょうか?具体的には、 16ms を超えてCPUメインスレッドが busy になると、クリックやスクロールなどが即座に反応しなくなります。いわゆる「もたつき」として認識されます。

off-the-main-thread は、言葉から分かる通り、マルチスレッド化するということより、メインスレッドではない、という部分に重きがあります。つまりこのもたつきを発生させる処理を別スレッドに逃すということです。

実際のソリューションは、既に主要なブラウザに搭載されていたが、今まであまり使われていなかった WebWorker を使おう、ということになります。使われなかった理由は、これらの問題が顕在化していなかったためと、JSでわざわざマルチスレッドを持ち込んでまで物事に向き合いたくない、といったあれそれがあったことでしょう。単に使われなかったので枯れていなかったので使われない、という鶏と卵の問題もあったように思います。

自分が思うに、今では ES Modules と Dynamic Import の導入によって、「ファイル名を指定して起動する」ということへの概念的なジャンプが、やや少なくなったように思います。

// ESM: Dynamic Import
const sub = await import('./sub.js')

// WebWorker
const worker = new Worker('./worker.js')

インラインで worker ロジックを記述できるようにしようぜ、という domenic (Promiseの仕様策定者)の tc39 への proposal もあります。

GitHub - domenic/proposal-blocks: A proposal for a new syntactic construct for serializable blocks of JavaScript code

こんな感じ

const result = await worker({|
  const res = await fetch("people.json");
  const json = await res.json();

  return json[2].firstName;
|});

(これはクロージャへの変数キャプチャのセマンティクスなどが問題になりそうですが…)

結果としてマルチスレッド化すると、マルチスレッドのデザインパターンを導入することもある、ということで、GoogleChromeLabs にはいくらか試験的なモジュールがあります。

github.com

github.com

Comlink はpostMessage のラッパーで、 Clooney はその上で アクターモデルを実装したものです。

UI スレッドでJS ロジックを書くということは、 document や window に触れないJavaScript を書く、ということで、 元は browser / node の抽象化手段だった Universal JavaScript の概念がまた別の役割を背負うようになるのではないでしょうか。

speakerdeck.com

個々のオフザメインスレッドへの向き合い方

Canvas

OffscreenCanvas という提案があります(Chromeで実装・有効化されています)。Canvas の所有権を Worker 側に移譲し、 Worker 側で DOM への操作を行います。

developer.mozilla.org

しれっと、所有権とか言いましたが、これは最近 WebWorker に導入された概念で、ArrayBuffer や ImageBitmap などのオブジェクトを、そのスレッドでの参照を手放すことを条件に別スレッドで使えるようにするインターフェースです。

developer.mozilla.org

(TypeScript に Rust の Borrowing がほしくなりますね…)

AMP

AMP でも Worker 側でJSを実行し、 amp-bind (ampの機能の一つでデータバインド機能) で実質的に任意なJSを実行できるようにしたい、という提案があります。

JS in AMP · Issue #13471 · ampproject/amphtml · GitHub

github.com

React

React v16 のアップデートでは内部がフルスクラッチで書き直され、内部的には非同期な rendering ができるようになっています。が、ユーザー側にはまだ開放されていません。

React v17 では Suspense と呼ばれる機能が導入され、Promise ベースの非同期読み込みや、重い rendering を別フレームに分割することが可能になります。

speakerdeck.com

ただ、これはブラウザ標準と協調して出てきた機能というより、自前でその機構を持とうとしているものに見えますね。WebWorker 使うというより、スレッド専有時間を分割するという発想です。

Vue

Vue の作者 Evan You の最近の記事では、内部の Mutation の適用には requestIdleCallback を使っていくという旨が書かれています。

medium.com

requestIdleCallback は、メインスレッドを専用しない限り裏に積まれたキューが実行される、優先度が低いコールバック登録です。

developer.mozilla.org

これもReactと同じく内部タスクのスレッド専有時間の分割です。

まとめ

話題になっている、といいつつも、実際はChorme周辺から、どうやったらWeb体験を良くできるか?ということから生まれた概念で、そのために WebWorker を使えないか試行錯誤している、という段階です。FMP, FCP, TTIといったメトリクスを改善するために、UIスレッド以外で処理を進めるしかない、といった経緯ですね。

末端の JSプログラマとしては、今後JSのマルチスレッド化が進む潮流が生まれようとしている、という理解をするといいと思います。Flux みたいなメッセージングベースの設計や、node 側から出てきた Universal JavaScript もおそらく有用なんじゃないでしょうか。

tensorflow.js の mnist (数値の画像分類) のコードを読んで勉強した

tfjs の練習がてら、ちょっとやってみた。大学生の頃PRMLを二章ぐらいまで読んでギブアップした程度の知識なので、ほぼ無知。

js.tensorflow.org

間違ってたら教えてほしい…というか特に予習とかせず生半可な知識でやったのでたぶん間違ってる。

入力と出力

  • 32(横)x32(縦)x2(白黒) の画像が入力値(数値が書いてある画像)
  • (なんかいろいろあって) 0 から 9 いずれかの数値に分類する

そのまま入力して関係を見出すのが困難なので、なんやかんやで次元を少なくする

  • 元の画像の入力を8x8 の二次元のベクトル(Tensor)に圧縮
  • 64の入力に対し、いずれかの数値に分類する

そういう仮説が事前にある、というのを受け入れるのが時間かかった。 さすがに画像を放り込むと、なんかすごいディープラーニング様ってやつが出力当ててくれるというものではない。

畳み込み(CNN)

  • ある x, y に対し、その周辺8マスを含む 3x3 の領域で白黒を判定する
  • 2x2 の領域を見て、その中間値で1の値に圧縮する(Pooling)
  • これを二回繰り返す(32*32/4/4=64)

というモデルを表すコードがこれ

function createConvModel() {
  // Create a sequential neural network model. tf.sequential provides an API
  // for creating "stacked" models where the output from one layer is used as
  // the input to the next layer.
  const model = tf.sequential();

  // The first layer of the convolutional neural network plays a dual role:
  // it is both the input layer of the neural network and a layer that performs
  // the first convolution operation on the input. It receives the 28x28 pixels
  // black and white images. This input layer uses 16 filters with a kernel size
  // of 5 pixels each. It uses a simple RELU activation function which pretty
  // much just looks like this: __/
  model.add(
    tf.layers.conv2d({
      inputShape: [IMAGE_H, IMAGE_W, 1],
      kernelSize: 3,
      filters: 16,
      activation: "relu"
    })
  );

  // After the first layer we include a MaxPooling layer. This acts as a sort of
  // downsampling using max values in a region instead of averaging.
  // https://www.quora.com/What-is-max-pooling-in-convolutional-neural-networks
  model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }));

  // Our third layer is another convolution, this time with 32 filters.
  model.add(
    tf.layers.conv2d({ kernelSize: 3, filters: 32, activation: "relu" })
  );

  // Max pooling again.
  model.add(tf.layers.maxPooling2d({ poolSize: 2, strides: 2 }));

  // Add another conv2d layer.
  model.add(
    tf.layers.conv2d({ kernelSize: 3, filters: 32, activation: "relu" })
  );

  // Now we flatten the output from the 2D filters into a 1D vector to prepare
  // it for input into our last layer. This is common practice when feeding
  // higher dimensional data to a final classification output layer.
  model.add(tf.layers.flatten({}));

  model.add(tf.layers.dense({ units: 64, activation: "relu" }));

  // Our last layer is a dense layer which has 10 output units, one for each
  // output class (i.e. 0, 1, 2, 3, 4, 5, 6, 7, 8, 9). Here the classes actually
  // represent numbers, but it's the same idea if you had classes that
  // represented other entities like dogs and cats (two output classes: 0, 1).
  // We use the softmax function as the activation for the output layer as it
  // creates a probability distribution over our 10 classes so their output
  // values sum to 1.
  model.add(tf.layers.dense({ units: 10, activation: "softmax" }));

  return model;
}

学習

画像とその数値の正解データセットが用意されてるので、それを一つずつ入力する。どの数値っぽいかという出力が出てくる。 出力に対し、正解の教師データと突き合わせて categoricalCrossentropy (ググると Multi-class logloss という名前のが知られている)で、正解との距離を対数で取ったものの和を誤差として用い、 LEARNING_RATE = 0.01 でバックプロパゲーション(誤差伝搬)させる。これでニューラルネット間の係数が修正されていく。

  // optimizer 定義
  model.compile({
    optimizer,
    loss: "categoricalCrossentropy",
    metrics: ["accuracy"]
  });

  // 訓練
  await model.fit(trainData.xs, trainData.labels, {
    batchSize,
    validationSplit,
    epochs: trainEpochs,
    callbacks: {
      onBatchEnd: async (batch, logs) => {
        trainBatchCount++;
        ui.logStatus(
          `Training... (` +
            `${((trainBatchCount / totalNumBatches) * 100).toFixed(1)}%` +
            ` complete). To stop training, refresh or close page.`
        );
        ui.plotLoss(trainBatchCount, logs.loss, "train");
        ui.plotAccuracy(trainBatchCount, logs.acc, "train");
        await tf.nextFrame();
      },
      onEpochEnd: async (epoch, logs) => {
        valAcc = logs.val_acc;
        ui.plotLoss(trainBatchCount, logs.val_loss, "validation");
        ui.plotAccuracy(trainBatchCount, logs.val_acc, "validation");
        await tf.nextFrame();
      }
    }
  });

  // 訓練されたモデルを使う
  const testResult = model.evaluate(testData.xs, testData.labels);

感想

CNN って想像していたニューラルネットっぽくないというか、これ単にヒューリスティックだった下処理に名前が付いてるやつでは?という気持ちになった。

なんか頑張って読むことは出来たが、自分で作れといわれてたら無理なので、素振りする必要がありそう。

SWSRSSR という1フレーム技

Service Worker Side React Server Side Rendering

ServiceWorker内でBabelを駆使して、JavaScriptをビルドする - ログミーTech(テック)

ってスライドの中で、React の SSR を Service Worker の中でやれば、SEO は死んじゃうけど First Meaningful Paint 最適化できるよねーっていう話をしました。

というわけで実装してみました。すごく単純な React + Redux の Counter です。

ここで試せる https://epic-cray-a9cff8.netlify.com

実装コード https://github.com/mizchi-sandbox/swsrssr

DevTools 見る限り、 FMP が 8ms で返ってます。TTI (JSが動くようになる時間) は JSの評価が終わる 150ms ぐらい。 ただ、ネットワーク全く使わないかわりに、処理速度が完全にCPU依存なので、モバイルだとここまではならないとは思います。

これはブラウザの標準的なフレームレートの16ms未満なので、リロードしたことをほぼ認識できない速度です。

この路線がありか、まだわからないけど、夢がありますね。やはり SW 用の express emulator 作ったほうがいい気がする。

WebRTC DataChannel で 1:1 接続をやってみた

まだプロトコルを正しく理解しておらず、とにかく動くところまで。そのメモ。

こんな感じ

https://i.gyazo.com/fb201b876ce130021c991a823e905d8d.gif

コード

GitHub - mizchi-sandbox/hello-data-channel

simple-peer と react で雑に一筆書きした。simple-peer は webtorrent などでも使われていて、筋が良さそう。

none => initiator | receiver => connected と mode が遷移する。

// <div class="root"></div> みたいなDOMがあることを前提

import Peer from "simple-peer";
import React from "react";
import ReactDOM from "react-dom";

let peer: Peer.Instance = null as any;

class Initiator extends React.Component<
  {
    onHandshaked: () => void;
  },
  {
    incoming: string;
    outgoing: null | string;
  }
> {
  state = {
    incoming: "",
    outgoing: null
  };

  textareaRef: any = React.createRef();

  componentDidMount() {
    peer = new Peer({ initiator: true, trickle: false });

    peer.on("error", (err: any) => {
      console.error(err);
    });

    peer.on("signal", (data: any) => {
      console.log("signal");
      this.setState(s => ({ ...s, outgoing: data }));
    });

    peer.on("connect", () => {
      console.log("CONNECT");
      // peer.send("hey, how is it going?");
      this.props.onHandshaked();
    });

    // peer.on("data", (data: any) => {
    //   console.log("data: " + data);
    // });
    return peer;
  }

  render() {
    return (
      <div>
        {this.state.outgoing && (
          <>
            <textarea
              ref={this.textareaRef as any}
              style={{ width: 600, height: 150, background: "#ddd" }}
              value={JSON.stringify(this.state.outgoing)}
              onChange={() => {
                /**/
              }}
            />

            <button
              onClick={() => {
                this.textareaRef.current.select();
                document.execCommand("copy");
              }}
            >
              Copy to clipboard
            </button>
            <hr />
            <textarea
              style={{ width: 600, height: 150 }}
              value={this.state.incoming}
              onChange={ev => {
                this.setState({ incoming: ev.target.value });
              }}
            />
            <button
              onClick={() => {
                peer.signal(JSON.parse(this.state.incoming));
              }}
            >
              signal
            </button>
          </>
        )}
      </div>
    );
  }
}

class Receiver extends React.Component<
  {
    onHandshaked: () => void;
  },
  {
    incoming: string;
    outgoing: null | string;
  }
> {
  state = {
    mode: null,
    handshaked: false,
    incoming: "",
    outgoing: null
  };

  textareaRef: any = React.createRef();

  componentDidMount() {
    peer = new Peer({ initiator: false, trickle: false });
    peer.on("error", (err: any) => {
      console.log("error", err);
    });

    peer.on("signal", (data: any) => {
      console.log("signal");
      this.setState(s => ({ ...s, outgoing: data }));
    });

    peer.on("connect", () => {
      console.log("CONNECT");
      this.props.onHandshaked();
    });

    return peer;
  }

  render() {
    return (
      <div>
        <div>
          <textarea
            style={{ width: 600, height: 150 }}
            value={this.state.incoming}
            onChange={ev => {
              this.setState({ incoming: ev.target.value });
            }}
          />

          <button
            onClick={() => {
              peer.signal(JSON.parse(this.state.incoming));
            }}
          >
            signal
          </button>
        </div>

        <div>
          {this.state.outgoing && (
            <>
              <textarea
                ref={this.textareaRef as any}
                style={{ width: 600, height: 150, background: "#ddd" }}
                value={JSON.stringify(this.state.outgoing)}
                onChange={() => {
                  /**/
                }}
              />

              <button
                onClick={() => {
                  this.textareaRef.current.select();
                  document.execCommand("copy");
                }}
              >
                Copy to clipboard
              </button>
            </>
          )}
        </div>
      </div>
    );
  }
}

class Chat extends React.Component<
  any,
  {
    text: string;
    comments: Array<{
      owner: string;
      text: string;
      date: number;
    }>;
  }
> {
  state = { text: "", comments: [] };

  componentDidMount() {
    peer.on("data", (data: any) => {
      const json = JSON.parse(data);
      this.setState({ comments: json });
    });
  }

  render() {
    return (
      <div>
        <textarea
          value={this.state.text}
          onChange={ev => {
            this.setState({ text: ev.target.value });
          }}
        />

        <button
          onClick={() => {
            const comment = {
              owner: (peer as any)._id,
              text: this.state.text,
              date: Date.now()
            };
            const newComments = [comment, ...this.state.comments];
            peer.send(JSON.stringify(newComments));
            this.setState({ text: "", comments: newComments });
          }}
        >
          send
        </button>

        <ul>
          {this.state.comments.map((c: any, index) => {
            return (
              <li key={index}>
                {c.owner}: {c.text}: {c.date}
              </li>
            );
          })}
        </ul>
      </div>
    );
  }
}

class App extends React.Component<
  {},
  {
    mode: "initiator" | "receiver" | "none" | "connected";
  }
> {
  state = {
    mode: "none"
  } as any;

  render() {
    switch (this.state.mode) {
      case "none": {
        return (
          <div>
            <button
              onClick={() => {
                this.setState({ mode: "initiator" });
              }}
            >
              Create connection
            </button>

            <button
              onClick={() => {
                this.setState({ mode: "receiver" });
              }}
            >
              Receive connection
            </button>
          </div>
        );
      }
      case "initiator": {
        return (
          <Initiator
            onHandshaked={() => {
              this.setState({ mode: "connected" });
            }}
          />
        );
      }
      case "receiver": {
        return (
          <Receiver
            onHandshaked={() => {
              this.setState({ mode: "connected" });
            }}
          />
        );
      }
      case "connected": {
        return <Chat />;
      }
    }
  }
}

ReactDOM.render(
  <>
    <h1>Handshaker</h1>
    <hr />
    <App />
  </>,
  document.querySelector(".root")
);

動いてるけど、Receiver 側でこんなエラーが出る

error Error: Ice connection failed.
    at makeError (main.16f9df1b.js:7031)
    at Peer._onIceStateChange (main.16f9df1b.js:6674)
    at RTCPeerConnection.Peer.self._pc.oniceconnectionstatechange (main.16f9df1b.js:6203)

stun しか設定してないので、そのせいだろうか。 trickle: false も怪しいが。プロトコルをちゃんと理解してないのでちゃんと調べる。

offer answer の流れを今はコピペしているが、自動化するには別途 firebase などでデータ受け渡さないといけないのかなーという気がする。そんなに難しくないので、あとでやる。

WEB+DB PRESS Vol.106 で仮想DOMアルゴリズムについて書かせていただきました

特集書かせていただきました

WEB+DB PRESS Vol.106

WEB+DB PRESS Vol.106

ターゲットは「仮想DOMアルゴリズムそのものに興味がある人」と「Reactのキャッチアップに遅れた人」

よくあるReactについての記事ではなく、仮想DOMアルゴリズムとそれによって実現されるFluxを解説する話です。Reactのコード例は出ますが、あくまで仮想DOMアルゴリズムそのものについて書いています。Vue や Flutter にもそのまま応用できるはずです。

本特集の内容を要約すると、仮想DOMアルゴリズムとは「木構造のダブルバッファリングで更新処理を枝刈りしたものを、その更新結果でGUIコンポーネントのライフサイクルを表現する」という話。

個人的な主張としては、Reactの仮想DOMアルゴリズムはWebだけに閉じた話ではなく、これからのGUIアーキテクチャを変えるもので、ReactやVueを使わなくとも、今学んでおくことは価値があると思います。

奇しくも Android iOSアプリ設計と同じ回ということで、GUIパラダイムについて掴んでおきたい人は読んでみてはいかがでしょうか。

よろしくおねがいします。

tweetdeck のタイムライン表示を user css で夜フクロウ風にした

usersteam 止まって、tweetdeck に移ったが、あまりにTLの一覧性が悪すぎたのでやった

こんな感じ

昔使ってた stylish が問題起こしてストアから削除されてたので、 stylebot を使った。

雑なCSS

.column:nth-child(1) {
    width: 600px;
}

.js-stream-item-content {
    padding: 0;
}

.fullname:not(:hover) {
    display: none;
}

.item-box:not(:hover) {
    height: 37px;
    overflow-x: auto;
    overflow-y: hidden;
}

.item-box:not(:hover) .tweet-context {
    display: none;
}

追記: ユーザータイムライン

1番目のカラム(設定によるがユーザータイムライン)だけに適用するようにした。Mentionなどの他のタブはたたむ必要ないと感じたので

.column:nth-child(1) {
    width: 600px;
}

.column:nth-child(1) .js-stream-item-content {
    padding: 0;
}

.column:nth-child(1) .fullname:not(:hover) {
    display: none;
}

.column:nth-child(1) .item-box:not(:hover) {
    height: 37px;
    overflow-x: auto;
    overflow-y: hidden;
}

.column:nth-child(1) .item-box:not(:hover) .tweet-context {
    display: none;
}