ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

Three.jsをTypeScriptと組み合わせて学んでみた

ブラウザ側のJavaScriptGPUに命令して3Dを表示できるThree.js というライブラリがあります。
Three.js – JavaScript 3D Library

このThree.jsをTypeScriptで動かしながら、いくつかのサイトを写経してみました。
今回はその時に参考にしたサイトと、JavaScriptで書かれているThree.jsの記事をTypeScriptのプロジェクトで書き直した時のメモを上げておきたいと思います。

Three.js + TypeScriptの環境について

環境については以下のサイトを参考にし、Three.js + TypeScrpt + Webpackという組み合わせとしました。
最新版TypeScript+webpack 5の環境構築まとめ(React, Vue.js, Three.jsのサンプル付き) - ICS MEDIA

Three.jsについて

以下のサイトをTypeScriptに変換しつつ写経しました。

Three.js入門サイト - ICS MEDIA
three.js超入門 第0回 3Dコンピュータグラフィックスの基礎 - Qiita

また他の例などが知りたければ、以下の公式サイトのサンプルを参考になりました。

https://threejs.org/examples/#webgl_geometry_convex

いくつか出たエラーについて

Access to image at 'xxx.jpg' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

CORSエラーですね。とりあえずローカルで簡易的なWebサーバを動かせばよかったので、今回は「goexec」を使いました。
Golangの開発環境がある場合ですが

go get -u github.com/shurcooL/goexec

で「goexec」をインストールし、以下のコマンドを開きたいHTMLファイルがあるフォルダで実行してWebサーバを起動しました。

$GOPATH/bin/goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))'

Three.jsのバージョン

Three.jsのバージョンを最新ではなく任意のバージョンにしたい場合などがあります。
以下のコマンドでnpmでインストール可能なバージョンを確認し、package.jsonに記述しました。

npm info three versions

Three.jsの参照

Webpackで必要なモジュールを纏めているので、ビルド前の.htmlファイルからはライブラリの参照の記述を削除しました。

OrbitControls

TypeScript用の型定義である@types がないOrbitControlsのモジュールを直接読み込むため以下を行いました。

npm install --save three-orbitcontrols

でインストールし、以下をmain.tsに記述します。

declare function require(x: string): any;
const OrbitControls = require('three-orbitcontrols');


以上です。次にまた自分がThree.jsを始めるためのメモとして残しておきます。

Shellで複数の結果をそのままforループさせる

普段はあまりシェルを書かないので手続き的に書こうとしてハマったのでメモ代わりに書いておきます。
タイトルにあるように、あるコマンドを実行して結果が複数返ってくるとします。
この結果をループして別処理を呼び出す引数にしようとしたら、思ったよりハマってしまいました。

結論としては、
1.あるコマンドを実行する
2.その1.の結果を配列などの変数に保持する
3.変数をループする

とするのではなく、

1.あるコマンドを実行する
2.それをそのままfor~inに渡す

とすれば良かったようです。
ソースを見た方が早いので以下にサンプルを上げます。

サンプル

以下のようなjsonをjqで解析して「name」の数だけループして値を別処理の引数とするとします。

sample.json

{
    "name": "name1",
    "description": "description1"
}
{
    "name": "name2",
    "description": "description2"
}
{
    "name": "name3",
    "description": "description3"
}

シェルは以下のようになりました。

sample.sh

#!/bin/sh

for value in $(cat sample.json | jq .name)
do
    echo "name = "$value
done

実行すると以下のように「name」の値を取得して別処理(今回はecho)に渡していることが分かります。

$ ./sample.sh
name = "name1"
name = "name2"
name = "name3"

まとめ

分かってしまえば簡単なのですが、jqの結果をそのままfor~inに渡すというのを直ぐに思いつきませんでした。
将来の自分や誰かの役に立てば幸いです。

追記

以下のように「declare -a」を使って最初に考えた結果を配列などの変数に保持する方法もできるようです。

sample2.sh

#!/bin/bash

declare -a ary=$(cat sample.json | jq .name)
for value in $ary
do
    echo $value
done

はじめてのFlutter - Drawer、List、State を使ってみる

タイトルにもあるようにFlutterを触り始めました。

Install - Flutter
こちらに「Get Started」のインストールから始め、公式サイトのチュートリアルをいくつかやってみた後で、理解を深めるために以下のようなサンプルをつくってみました。

右下の「+」を押すとカウントが増える
f:id:UnderSourceCode:20210124121434p:plain

上の画面の左上をタップすると Drawer が開く
f:id:UnderSourceCode:20210124122248p:plain

「Item List」をタップするとリストが表示され、「+」を押した数だけ「Item N」が増える
f:id:UnderSourceCode:20210124122355p:plain

これらはFlutterのプロジェクトを「New Application Project」から作成した雛形と、以下の「Add Drawer」のチュートリアルにあるソースを合体させたものに手を加えてつくりました。
Add a Drawer to a screen - Flutter

タイトルにあるようにFlutterのDrawer、List、Stateを使っています。以下にソースを書いておきます。

私自身は公式のチュートリアルをやった後に以下を実装してみました。公式チュートリアルをやったあと、以下の例など色んなサンプルを動かしてみることで理解が早まる気がします。

ソースとDrawer、List、Stateについて

ソースは分かりやすいよう「lib/main.dart」に纏めてみました。ローカルで試す場合もプロジェクトを作成して以下のソースを張り付ければ動くかと思います(バージョン変更などなければですが・・・)

lib/main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Drawer List Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  List<String> _listItems = new List<String>();

  void _incrementCounter() {
    setState(() {
      _counter++;
      _listItems.add('Item $_counter');
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      // add drawer
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: _createDrawerList(),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

  List<Widget> _createDrawerList() {
    return <Widget>[
      DrawerHeader(
        child: Text('Drawer Header',
            style: TextStyle(color: Colors.white, fontSize: 20)),
        decoration: BoxDecoration(
          color: Colors.blue,
        ),
      ),
      ListTile(
        leading: Icon(Icons.home),
        title: Text('Home'),
        onTap: () {
          Navigator.pop(context);
        },
      ),
      ListTile(
        leading: Icon(Icons.list),
        title: Text('Item List'),
        onTap: () {
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => ListScreen(listItems: _listItems),
            ),
          );
        },
      ),
    ];
  }
}

class DetailScreen extends StatefulWidget {
  final DetailRecord detailRecord;

  DetailScreen({Key key, @required this.detailRecord}) : super(key: key);

  @override
  _DetailScreenState createState() => _DetailScreenState();
}

class _DetailScreenState extends State<DetailScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.detailRecord.title),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Text(widget.detailRecord.text),
      ),
    );
  }
}

class DetailRecord {
  String title;
  String text;
  DetailRecord(String title, String text) {
    this.title = title;
    this.text = text;
  }
}

class ListScreen extends StatefulWidget {
  final List<String> listItems;

  ListScreen({Key key, @required this.listItems}) : super(key: key);

  @override
  _ListScreenState createState() => _ListScreenState();
}

class _ListScreenState extends State<ListScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Item List'),
      ),
      body: ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          return Container(
              decoration: BoxDecoration(
                border: Border(
                  bottom: BorderSide(color: Colors.black38),
                ),
              ),
              child: ListTile(
                leading: const Icon(Icons.info),
                title: Text(widget.listItems[index]),
                onTap: () {
                  DetailRecord detailRecord =
                      new DetailRecord("Item Detail", widget.listItems[index]);
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) =>
                          DetailScreen(detailRecord: detailRecord),
                    ),
                  );
                },
              ));
        },
        itemCount: widget.listItems.length,
      ),
    );
  }
}
Drawer

メインとなる最初の画面を作る「_MyHomePageState」クラスの「build」メソッドにて、文字通り「drawer」について記述している箇所があります。その中で「_createDrawerList」というWidgetのListを作成するメソッドを呼び出しています。

この部分がDrawerを作っている所となります。物凄い大雑把な説明になってしまうのですが、要は複数のWidgetをListで集める形でDrawerを作っているような感じです。

List

「+」をタップした数の「Item N」を表示するリストは、「ListScreen」クラスとその状態を保持する「_ListScreenState」クラスで構成しています。リストに表示する複数の値は「listItems」というString型のListとして保持しています。この「listItems」をリストを表示する「ListView.builder」で使うことでリスト形式での表示を行っています。

「listItems」は「+」をタップするごとに数が増えるのですが、それについては次の「State」で触れます。ちなみにリストで表示される「Item N」をタップすると簡単な詳細画面に遷移するようにもしてあります。ソースを動かせる方はやってみてください。

State

State = 状態ですが、今回のように「+」をタップする度に動的に増える値などはStateとして保持します。「+」ボタンはメインとなる最初の画面の「_MyHomePageState」クラスの「build」メソッドにて「floatingActionButton」の箇所にて定義しています。

この「_MyHomePageState」クラスですが、Stateを保持するよう「class _MyHomePageState extends State」のように「State」を継承して作られています。

「+」をタップした数とリストに表示する値は、Staetを継承した「_MyHomePageState」クラス内のメンバ変数「_counter」「_listItems」で保持します。「+」をタップした時に呼ばれる「_incrementCounter」メソッドを見ると「setState」内で「_counter」「_listItems」がそれぞれ増えている or 追加していることが分かるかと思います。

先にリストを表示する「ListScreen」クラスを説明したところで「listItems」というString型のListを表示していると書きましたが、「builder: (context) => ListScreen(listItems: _listItems)」の箇所でStateとして保持していた「_listItems」を「ListScreen」クラスに渡しています。

Golangのx.text.transformが便利だった

タイトルにある様にGolangのx.text.transformパッケージについてです。
このパッケージを使うと、readerやwriter内のデータを変換することができます。
良くある用途としては、readerで読み込んだ文字列の一部を変換して出力する、などがあると思います。

このパッケージを知った切っ掛けは以下のSlideShareです。
オススメの標準・準標準パッケージ20選

SlideShareで言及されていますが、パッケージのドキュメントは以下となり、Exampleを見れば使い方は分かるかと思います。
https://godoc.org/golang.org/x/text/transform#Transformer
https://godoc.org/github.com/tenntenn/text/transform#example-ReplaceTable

transformパッケージ自体については上記のリンクを見れば分かるのですが
自分が理解するために書いたサンプルソースもメモ代わりに上げておきます。

Golangで2つのgzファイルを連結してみる - ソースコードから理解する技術-UnderSourceCode
以前書いた上記のソースを改修したもので、2つの圧縮ファイル(中身はjson)を読み込み、値を変換して、圧縮ファイルに出力するものです。

サンプルソース

「sample1.json.gz」「sample2.json.gz」というgzファイルをあらかじめ用意しておき、実行すると中身が連結されて「result.json.gz」というファイルに出力されます。

sample1、sample2のjsonの中身は以下のような形式で、「abc」「fgh」をそれぞれ大文字に変換して出力しています。

sample1

{
    "data":"abcde"
}

sample2

{
    "data":"fghij"
}

メインの処理は以下のようになります。

package main

import (
	"compress/gzip"
	"fmt"
	"io"
	"log"
	"os"

	. "github.com/tenntenn/text/transform"
	"golang.org/x/text/transform"
)

func write(br1 io.Reader, br2 io.Reader) error {
	writeFile, err := os.OpenFile("./result.json.gz", os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		return err
	}
	defer writeFile.Close()

	zw := gzip.NewWriter(writeFile)
	defer zw.Close()

	_, err = io.Copy(zw, br1)
	if err != nil {
		return err
	}

	_, err = zw.Write([]byte("\n"))
	if err != nil {
		return err
	}

	_, err = io.Copy(zw, br2)
	if err != nil {
		return err
	}

	return nil
}

func doTransform(zr *gzip.Reader, old, new []byte) io.Reader {
	t := ReplaceByteTable{
		old, new,
	}
	return transform.NewReader(zr, ReplaceAll(t))
}

func main() {
	readFile1, err := os.Open("./sample1.json.gz")
	if err != nil {
		log.Fatal(err)
	}
	defer readFile1.Close()

	zr1, err := gzip.NewReader(readFile1)
	if err != nil {
		log.Fatal(err)
	}
	defer zr1.Close()

	readFile2, err := os.Open("./sample2.json.gz")
	if err != nil {
		log.Fatal(err)
	}
	defer readFile2.Close()

	zr2, err := gzip.NewReader(readFile2)
	if err != nil {
		log.Fatal(err)
	}
	defer zr2.Close()

	tr1 := doTransform(zr1, []byte(`abc`), []byte(`ABC`))
	tr2 := doTransform(zr2, []byte(`fgh`), []byte(`FGH`))

	err = write(tr1, tr2)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("-----finish-----")
}

transformパッケージを使っているのはdoTransform()メソッドになります。
.gzファイルを読み込んだreaderを受け取り、変換するルールをReplaceByteTableに定義し、新たに別のreaderを生成して返却しています。

返却したreaderはwrite()に渡し、そのままresult.json.gzに出力しています。

このような感じにreaderの中身を変換して出力できることが確認できました。

localstackのローカル環境にAWS CDKでLambdaを配置してみた & Windows 10 でGolangのLambdaを作った

前回に続き、またlocalstackについてです。タイトルにある様にLambdaをデプロイしてみたのですが、Windows 10 で GolangのLambdaを作ったため、そこでも知らないことが出てきたのでメモ代わりに書いておきます。

localstackの起動

GitHub - localstack/localstack: 💻 A fully functional local AWS cloud stack. Develop and test your cloud & Serverless apps offline!

localstackをgitでcloneし、以下のコマンドで起動しておきます。

$ git clone https://github.com/localstack/localstack
$ cd localstack
$ docker-compose up

CDKのプロジェクトの構成

aws cdk でGolangのLambdaをデプロイしてみる - ソースコードから理解する技術-UnderSourceCode

この時に書いたときと同じ構成ですが、後に書くlambdaのビルド用バッチファイルを「main.go」と同階層に置きました。以下のような構成となります。

- lambda/ ・・・ 新規で作るフォルダ
  - bin/
     - main ・・・ main.goをビルドしたバイナリ。後述します。
  - main.go ・・・ Lambdaのソース
  - build.bat ・・・ Lambdaビルド用バッチファイル
- lib/
  - cdk-lambda-stack.ts
- node_modules/
- README.md

Lambda本体である「main.go」は、先の記事と同じものです。

lambdaのビルド

先に書いたようにWindows 10 で GolangのLambdaを作ったのですが、以下を参考に「build-lambda-zip」を使って圧縮してやる必要があるようでした。
Go の AWS Lambda デプロイパッケージ - AWS Lambda

「build-lambda-zip」を go get~で取得したあと、以下のようなバッチファイルをつくり、Lambdaのビルドは一発でできるようにしました。

build.bat

env GOOS=linux go build -o bin/main main.go
cd bin
build-lambda-zip.exe -output main.zip main

CDK

CDKについてはlambdaのビルド時に圧縮したzipをデプロイできるよう、「lib/cdk-lambda-stack.ts」を以下のようにしました。

lib/cdk-lambda-stack.ts

import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';

export class CdkLambdaStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
    new lambda.Function( this, 'GoFunction', {
      functionName: 'GoFunction',
      runtime: lambda.Runtime.GO_1_X,
      handler: 'main',
      code: lambda.Code.fromAsset('./lambda/bin/main.zip')
    })
  }
}

デプロイと動作確認

localstackにデプロイするので、「cdklocal」コマンドを使用します。「cdklocal」については前回などを参考にしてください。以下のようなコマンドとなります。

# デプロイ
$ cdklocal bootstrap --endpoint-url=http://localhost:4566 --profile=localstack
$ cdklocal deploy --endpoint-url=http://localhost:4566 --profile=localstack

# Lambdaの動作確認
$ aws lambda invoke --function-name GoFunction --endpoint-url http://localhost:4566 --profile localstack response.json

localstackのローカル環境にAWS CDKでS3バケットを作成してみる

GitHub - localstack/localstack: 💻 A fully functional local AWS cloud stack. Develop and test your cloud & Serverless apps offline!
「localstack」というAWSを仮想的にローカルマシン内のDockerで動かすものがあります。

このlocalstack内に、AWS CDKを使ってS3バケットを作成してみました。

localstackの準備

localstack と Visual Studio Code の Remote-Containers でAWSの開発環境を構築してみる - ソースコードから理解する技術-UnderSourceCode
以前にも書いたように、git cloneでlocakstackを取得し、dockerで起動します。
詳細な手順は公式や上記を参照して欲しいですが、コマンドだけ書くと以下のようになります。

$ git clone https://github.com/localstack/localstack
$ cd localstack
$ docker-compose up

インストール後、クレデンシャルファイルに プロファイル名「localstack」の定義を作成しておきます。

AWS CDKとAWS Cloud Development Kit (CDK) for LocalStack(aws-cdk-local)

AWS CDKだけ先に用意し、CDKから直接 endpoint と プロファイルを指定してデプロイしようとしたのですが、以下のようなエラーメッセージができませんでした。

Unable to resolve AWS account to use. It must be either configured when you define your CDK or through the environment

locakstackより、「AWS Cloud Development Kit (CDK) for LocalStack」(aws-cdk-local)というのが出ているようなので、こちらを使うことにしました。
GitHub - localstack/aws-cdk-local: Thin wrapper script for using the AWS CDK CLI with LocalStack

公式ではローカルにnpmでインストールしているようですが、今回はAWS CDK・aws-cdk-local ともグローバルにインストールしました。

$ npm install -g aws-cdk
$ npm install -g aws-cdk-local
$ cdklocal --version

AWS CDKのプロジェクトは、以前の記事や公式サイトを元に作成しました。CDKでS3バケットを作成するだけのプロジェクトです。
aws-cdkを触ってみた - ソースコードから理解する技術-UnderSourceCode
Your first AWS CDK app - AWS Cloud Development Kit (AWS CDK)

npmによるビルドや、synthによるCloudFormationの確認などは通常のAWSをCDKで操作するときと同じように行うことが出来ます。
endpoint と プロファイルを localstackのものを指定してます。

$ npm run build
$ cdk synth --endpoint-url=http://localhost:4566 --profile=localstack

デプロイについては、aws-cdk-locakのコマンド「cdklocal」を使い、以下のように行います。

$ cdklocal deploy --endpoint-url=http://localhost:4566 --profile=localstack

デプロイされたことを確認するため、aws cliのコマンドを使って確認してみます。
ここでも endpoint と プロファイルを localstackのものを指定してます。

$ aws s3 ls --endpoint-url=http://localhost:4566 --profile=localstack
2020-11-15 15:21:50 hello-cdk-20201115

以上です。

JavaScriptにTypeScriptの型チェックを導入してみる

TypeScript: Handbook - Type Checking JavaScript Files
このようなサイトを見つけたので試してみました。公式なので当然かもしれませんが、上記のサイトに書いてあるようなJavaScriptファイルにTypeScriptの型チェックをできました。

以下、やったことのメモ書きです。

前提条件

Win10 + Visual Studio CodeJavaScript(TypeScript)の実行環境はVisual Studio CodeのRemote Containersとしました。ですが、Remote Containers以外はOS、エディタに関わらずできるはずです。

環境構築

JavaScriptの実行環境として「vscode-remote-try-node」をgit cloneし、Visual Studio CodeのRemote Containersで起動します。この時点でコンテナにはnode、npmがインストールされており、JavaScirptがデバッグ実行できるはずです。

次にTypeScriptを導入するために、以下のコマンドを実行します。

$ npm install -g typescript
$ tsc --init

tsc --init」で「tsconfig.json」が作成されますが、今回はJavaScriptに対してtscでのチェックを行いたいので、ファイル名を「jsconfig.json」に変更します。JavaScriptに対応させるため「jsconfig.json」を以下のように変更しました。(コメントは変更した理由など)

{
  "compilerOptions": {
(中略)
    "checkJs": true, // JavaScirptのチェックを行う
(中略)
    "noImplicitAny": false, // メソッドの引数でany型を指定していない場合のエラーを回避する
(中略)

tscコマンドを実行するか、Visual Studio CodeならTypeScript用のプラグインを入れることで、型チェックが行われて(エラーがあれば)適切なエラーが表示されると思います。

とりあえず最初に紹介した公式のサンプルソースで試してみましたが、「this.constructorOnly」の型エラーは表示されることを確認しました。またメソッドに引数を追加してみたところany型を指定していない旨のエラーとなったので、これについては表示されないようjsconfig.jsonを修正しています。

色んなパターンのJavaScriptのプログラムを試したわけではありませんが、一応、TypeScriptの型チェックがJavaScriptファイルに対してできたみたいです。