焼売飯店

GoとかTS、JSとか

tsconfig.jsonはJSONじゃないと言う話

気になったので調べてみました。

tsconfig.jsonと普通のJSONの大きな違い

tsconfig.jsonには、コメントが書けます。

tsc --init した時に生成されるtsconfig.jsonに、大量にコメントが付けられているので、すぐに気付くことと思います。

例)

{
  "compilerOptions": {
    "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
    "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,

    /* Strict Type-Checking Options */
    "strict": true /* Enable all strict type-checking options. */,
    "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */
  }
}

これについて深く考えたことも無く、どこかでこれは JSON5 だと言う噂を見ていた*1のでそうだと思っていました。

ところが、試してみたところJSON5なら使えるはずの豊富な文法が全然使えなかったり、JSONとJSON5にかけていたformatOnSaveの設定が効かなかったので、違うということに気付きました。

続きを読む

Goの標準パッケージのwasmサイズを計測した

Go 1.11でgo buildのtargetに

  • GOOS=js
  • GOARCH=wasm

を指定できるようになって久しいですが、皆さんこちらお使いでしょうか?

Goが出力するWebAssemblyのファイルサイズが気になり、 これを計測するためのリポジトリgo-wasm-sizesを作ったので、こちらと合わせて実際の計測結果を紹介します。

github.com

wasmのサイズ計測

やっている事は単純で、

  • Goの標準パッケージのリストを書き出す
  • それらをimportだけして使用しないコードを生成する
  • 全てwasmにビルドする

作業を行っているだけです。

コード例

package main

import _ "fmt"

func main() {}

また、比較対象として、何のpackageもimportしていないパターンも用意しました。

計測方法について追記 (2018/12/12)

渋川さんからご指摘いただきましたが、今回の計測方法だとdead code eliminationされている可能性がありそうなので、こちらも考慮した上で別途検証したいです。

計測結果

よく使いそうなpackageを適当に選んで紹介します。 計測対象にしている全packageのリストは、上記リポジトリのREADME.mdに記載があります。

※net/httpなどの子packageは、作業の簡略化のために / ではなく _ で連結した名前を設定しています。

1.30MB blank/blank.wasm # This package only contains func main().
1.30MB math/math.wasm
1.30MB strconv/strconv.wasm
1.31MB io/io.wasm
1.34MB syscall_js/syscall_js.wasm
1.43MB time/time.wasm
1.44MB unicode/unicode.wasm
1.45MB strings/strings.wasm
1.45MB bytes/bytes.wasm
1.45MB path/path.wasm
1.46MB bufio/bufio.wasm
1.50MB os/os.wasm
1.98MB reflect/reflect.wasm
1.98MB sort/sort.wasm
2.00MB regexp/regexp.wasm
2.15MB fmt/fmt.wasm
2.16MB net_url/net_url.wasm
2.16MB context/context.wasm
2.37MB net/net.wasm
7.22MB net_http/net_http.wasm
11.32MB net_http_pprof/net_http_pprof.wasm

わかったこと

  • 何もimportしなくてもwasmのサイズは1.3MBになり、一般的なJSファイルと比べるとかなり大きい。
  • fmtをimportするとサイズが +800KB となり意外と大きく(恐らく内部でreflectを使っているため)、手軽に使うには向かない。
    • builtinprintln() が JSコンソールに表示を行うので、Goで実行した内容をコンソールに出したいだけであればそちらを使うのがよさそう。
  • strconv, math等はほとんどサイズに影響を及ぼさない。何もimportしない状態で既にビルドに含まれているのかも?

一番大きかったpackageは net/http/pprof で、なんと 11.32MB になりました。

圧縮するとどうなる?

生のままだとファイルサイズが大きすぎるため、実際にGoのwasmを使用する場合は、ブラウザがサポートする形式で圧縮して送信するのが現実的な方法となりそうです。

今回は、Gzip, Brotliの2つで、圧縮結果のサイズを比較してみました。

Gzip

287KB blank/blank.wasm.gz # This package only contains func main().
287KB math/math.wasm.gz
287KB strconv/strconv.wasm.gz
290KB io/io.wasm.gz
295KB syscall_js/syscall_js.wasm.gz
307KB time/time.wasm.gz
322KB unicode/unicode.wasm.gz
323KB os/os.wasm.gz
324KB strings/strings.wasm.gz
324KB bytes/bytes.wasm.gz
324KB path/path.wasm.gz
325KB bufio/bufio.wasm.gz
414KB reflect/reflect.wasm.gz
414KB sort/sort.wasm.gz
418KB regexp/regexp.wasm.gz
444KB fmt/fmt.wasm.gz
445KB net_url/net_url.wasm.gz
446KB context/context.wasm.gz
497KB net/net.wasm.gz
1617KB net_http/net_http.wasm.gz
2455KB net_http_pprof/net_http_pprof.wasm.gz

Brotli

222KB blank/blank.wasm.br # This package only contains func main().
222KB math/math.wasm.br
223KB strconv/strconv.wasm.br
224KB io/io.wasm.br
228KB syscall_js/syscall_js.wasm.br
237KB time/time.wasm.br
247KB unicode/unicode.wasm.br
249KB os/os.wasm.br
249KB strings/strings.wasm.br
249KB bytes/bytes.wasm.br
249KB path/path.wasm.br
250KB bufio/bufio.wasm.br
312KB reflect/reflect.wasm.br
313KB sort/sort.wasm.br
316KB regexp/regexp.wasm.br
336KB fmt/fmt.wasm.br
336KB net_url/net_url.wasm.br
336KB context/context.wasm.br
375KB net/net.wasm.br
1768KB net_http_pprof/net_http_pprof.wasm.br

わかったこと

最小のblank.wasm, 最大のnet_http_pprof.wasmでそれぞれ比較すると、

blank.wasm

  • Gzip: 22% (287KB)
  • Brotli: 17% (222KB)

net_http_pprof.wasm

  • Gzip: 22% (2455KB)
  • Brotli: 16% (1768KB)

となっており、どちらの圧縮法を使っても20%程度にはなるようです。 これだけファイルサイズが変わるのであれば、体感のロード時間は大きく変わるので、やはりGoのwasmの配信は圧縮した上で行うのが基本的な方針になりそうです。

Go + wasmで実際に何か作られる際にぜひ参考にしてください!

Vue.js (+ Vuex)でTrelloもどきを作った話

久しぶりの更新です。

先週末と今週末の二日を使って、Vuexの勉強がてらTrelloもどきを作ってみました。 これまでちまちまVue.jsは使ってきましたが、SPAを作ったのは初めてです(小さいですが)

プレビュー

f:id:f_syumai:20170320004732g:plain

デモの方はlocalStorageにデータを溜めるようになっているので、リロードしてもちゃんと前回の結果が残ってます。

実装内容

  • リストの追加、削除
  • カードの追加、削除
  • カードの左右リストへの移動

感想とか

Vuexのドキュメントめっちゃ読みやすい

Vuexは、ざっくり言うと、Vueのために作られたFluxに相当するライブラリ(詳細はこちら)です。Fluxもそのまま使えると思いますが、VuexはVueに特化したヘルパーメソッド等がふんだんに含まれているライブラリとなっています。

FluxとReduxの公式ドキュメントが英語のみ(確か)だったのに対し、Vuexの公式ドキュメントは全編日本語化されているため、めっちゃ読みやすいです。 いや、頑張って英語読めよ、と言う話ではあると思いますが、読むスピードと理解度のことを考えるとやはり母語で読めるのはありがたい…。

スコープCSS便利

生のJSじゃなくて、*.vueからトランスパイルするようにすると、各コンポーネントに対してスコープを制限したCSS(webpack使えばSCSSとかもいける)を付けられます。

サンプル: trollo/App.vue at master · syumai/trollo · GitHub

コンポーネントの定義とCSS定義が単一ファイルにまとまるので、整理整頓が徹底され、今回のような複数コンポーネントを組み合わせて作るSPAには向いていると感じました。(PolymerとかRiot.jsとかも同じだと思いますが…。)

React使ってた時よりforEachとか三項演算子とか使う機会が減った気がする

v-forとかv-ifのおかげで、React.Componentのrender()でforEachとかmapとか三項演算子で頑張ってたコードがHTML側に閉じ込められるので、JSとHTMLが混在しにくくなっている感じがしました。

続き

ドラッグでカードの移動とかもやってみたいので、来週あたりやれればと思います。

Rubyでdotenv的なやつを小さなスクリプトで実装してみる

手元のプロジェクトで環境変数をファイルから読み込む必要が出てきて、dotenvが欲しくなったのですが、ただそれだけのために依存関係増やすのもなぁ…と思ったので、自分で書いてみることにしました。

コード

envloader.rb

class Envloader
  class << self
    def load(filepath = File.expand_path('../.env', __FILE__))
      File.open(filepath) do |f|
        f.each_line do |line|
          if line =~ /\=/
            key, value = line.split('=').map(&:strip)
            value = value.match(/[\'\"]?([^\'\"]+)[\'\"]?/)[0] # 引用符を削除
            ENV[key] = value
          end
        end
      end
    end
  end
end

長さは15行です。(短い!)

使い方

1. .envファイルを作る

設定したい環境変数を含んだ.envファイルをenvloaderと同じディレクトリに置きます

.env

USERNAME=username
PASSWORD="secret" # 引用符も使える

2.使いたいスクリプト中で読み込む

main.rb

require './envloader'

Envloader.load

ENV['USERNAME'] #=> "username"

実装の方針

やりたかったのは以下の内容でした。

  1. .envにgitで追跡したくない情報を入れておいてスクリプト中で読み込みたい
  2. .envの書き方は hoge=huga もしくは、右辺を ''"" でくくったやつだけで、一つにつき一行使う
  3. 読み込んだ環境変数はENVに入るようにする

これに加えて、.envファイルのパスを指定できる機能もついてますが、流れで入れただけなので恐らくそこまで使わないかと思います…。

追加実装するなら、プロジェクトルートの自動認識とかでしょうか。

ここ最近は、こんな感じの細々したスクリプトを書いて楽しく過ごしています(笑

ドットでチェーンして値を追加していけるHashを実装してみた

手抜き実装なので、まだ色々足りて無さそうな感じがしますが、一旦の目的までは達成出来ています。

名前はChainableHashとしてみました。

コード

class ChainableHash < Hash
  def method_missing(name, *args, &block)  
    if name[-1] === '='
      self[name[0..-2].to_sym] = args.first
    elsif self.has_key?(name.to_sym)
      self[name.to_sym]
    else
      self[name.to_sym] = self.class.new
    end  
  end  
end

使い方

config = ChainableHash.new
config.environment = 'development' # environmentキーと値の追加
p config
# => {:environment=>"development"}

config.capacity.max = 10 # capacityを定義していなくてもそのままmaxまで指定できる
config.capacity.min = 5  # minキーの追加も簡単に出来る
p config
# => {:environment=>"development", :capacity=>{:max=>10, :min=>5}}

ポイント

else
  self[name.to_sym] = self.class.new
end  

ここの部分で、キーが存在しなかった場合にChainableHashのインスタンスを値としてセットするようにしています。 このおかげでチェーンが可能になっています。

Docker HubにPushしたイメージをAmazon ECSにデプロイしてみる

Docker HubにイメージをPushしてみる - 焼売飯店の続きです。

かなりアッサリ出来ました。

手順

1.AWSコンソールからECSを選択

f:id:f_syumai:20170114122335p:plain これら2つをガイド付きで実行できるようになっています。

ただし、今回はこのどちらも使わない(ECR使わないし、サンプルアプリも不要なので)ので、キャンセルを押して次に進みます。

2.タスクの登録

f:id:f_syumai:20170114122638p:plain ECSでイメージを実行するには、タスクの登録が必要になります。 実行するイメージを登録(複数可)したり、リンクさせたりなどの設定をこちらで行います。 Docker Composeで言うdocker-compose.ymlみたいな物だと考えるとわかりやすい気がします。 こちらでは、コンテナのメモリ、CPU数も設定できます。

f:id:f_syumai:20170114122952p:plain イメージには、前回Docker HubにPushしたsyumai/sinatra-alpine:0.1.0を指定しています。 シンプルなSinatraアプリなので、メモリ128MB、CPU数1で登録しました。

3.クラスターの作成

タスクを実行するためのクラスターを作ります。 こちらは、コンテナを実行するインスタンスの数、種類、スケーリング方法などについて設定を行います。 試験的にデプロイしたいだけなので、インスタンス数1のt2.microで作りました。

セキュリティグループに関しては、インバウンドで80番を受け付けるように設定します。

4.タスクの実行

こちらのクラスタで、先ほど作ったsinatra-alpineのタスクを実行してみます。 クラスタの作成と同時にコンテナ実行用のEC2が一つ立ち上がるのですが、2,3分ほどかかるので、少し待ってから実行する必要があります。

f:id:f_syumai:20170114123516p:plain

こちらも、あっさり動きました!

まとめ

実行するだけなら本当に簡単に出来てしまいました。 本番環境で使うとなると、今回タスクの実行で起動したものをサービス(今回は使いませんでした)で起動するようにしたり、複数コンテナを連携させたり、ELB(ロードバランサ)を追加してスケーリングに対応できるようにしたりする事になるかと思います。 この辺りは追い追いやっていこうかと思います。

Docker HubにイメージをPushしてみる

SinatraアプリのHello world on Docker with Alpineの続きです。

前書き

前回作ったSinatraアプリのコンテナを、Amazon EC2 Container Services (以下、ECS) にデプロイ出来るようにしたいと思います。 ECSから自分のイメージを使えるようにするためには、どこかしらのレジストリにイメージを配置する必要があります。

イメージの置き場所について

ECSから読めればどこでも良い(自前のリポジトリも可?)ですが、 最も一般的なのはAmazon本家のEC2 Container Registry(ECR)です。

今回は、手軽に無料で使えるDocker Hubを使いますが、無料では公開リポジトリしか置けないですし、 ECRにイメージを置いておけば、同一リージョン内からのイメージのpull、すなわちEC2へのデプロイで料金が発生しないので、ECSを使う上では非常にお得です。

ECRの料金は次の項目で発生します。

  • ストレージ料金 => 0.10 USD/GB/月
  • イン(Push) => 0円
  • アウト(同一リージョン外からのPull) => 最初の1GB無料、それ以降 0.140 USD/GB (月に10TBまでの場合)

気軽に試す分には、維持費のかからないDocker Hubでもいいかなと思います。

手順

1.Docker Hubにリポジトリを作る

今回は、syumai/sinatra-alpineとしました。

f:id:f_syumai:20170113023005p:plain

2.タグ付きでビルド

docker build -t syumai/sinatra-alpine:0.1.0 ./

3.Docker Hubにログイン

docker login
(ユーザー名、パスワードの入力)

4.Pushする

docker push syumai/sinatra-alpine:0.1.0

Push完了!

Pushしたイメージを確認する

Pushしたイメージは、Docker Hub上の自分のページに、あれ、見つからない…。 でもdocker pull syumai/sinatra-alpine:0.1.0は動くので、Pushは成功しているっぽいです。 どっか間違ったのか時間差があるのでしょうか…。また明日確認してみます。

次回は、こちらのアップロードしたイメージ or ECRを使ってECSにデプロイしてみます。

追記(2017/1/14)

6時間後くらいに見たら、ちゃんとDocker Hubのページに反映されていました。