コピペコードで快適生活

明日使えるソースを自分のために

ReactHooksを使ってみる

最近のReactでは関数コンポーネント&ReactHooksを使うのが主流らしい。
キャッチアップしたところ、state管理がコンポーネントからキレイに分離できてよさそうだった。

index.tsx

import * as React from "react";
import * as ReactDOM from "react-dom";
import Omikuji from "./Omikuji";

let element: JSX.Element = <Omikuji name="kinosuke01"/>;
let dom: HTMLElement = document.getElementById('root');
ReactDOM.render(element, dom);

Omikuji.tsx

import React from 'react';
import useOmikuji from './useOmikuji';

interface Props {
  name: string,
};

interface State {
};

export default function Omikuji(props: Props) {
  // state管理を行う処理を、関数コンポーネントに組み込む。
  let [result, updateResult] = useOmikuji();

  let message: string = '今日の運勢を占います!';
  let buttonText: string = '占う!';
  if (result) {
    message = `${props.name}さんの今日の運勢は「${result}」です!!`;
    buttonText = 'もう一度占う!';
  }

  return (
    <div>
      <p>{message}</p>
      <button onClick={() => updateResult()}>{buttonText}</button>
    </div>
  )
};

useOmikuji.ts

import { useState, useEffect } from 'react';

//
// 関数コンポーネントにstate管理を組み込む関数。
// state管理をコンポーネントから分離できる。
// 複数のコンポーネントで使い回すことができる。
// setResultやuseEffectを使うことで実現。
//
export default function useOmikuji(): [string, Function] {
  // result -> state.result
  // setResult -> setState({result: xxx});
  // に対応するイメージ
  // useStateの引数は、対象stateの初期値
  const [result, setResult] = useState(null);

  function predict(): string {
    let i: number = Math.floor(Math.random() * 10 % 5);
    let list: Array<string> = ['大吉', '吉', '中吉', '小吉', '凶'];
    return list[i];
  }

  function updateResult(): void {
    setResult('占い中...');
    setTimeout(() => {
      let predictResult: string = predict();
      setResult(predictResult);
    }, 1000);
  }

  // useEffectには、
  // componentDidMount, componentDidUpdate の
  // タイミングで呼びたい関数を渡す
  useEffect(() => {
    console.log(`「${result}」を取得しました。`);

    // 戻り値には、componentWillUnmountの
    // タイミングで呼びたい関数を渡す
    return (): void => {
      console.log('さよなら');
      return;
    };
  });

  return [result, updateResult];
}

参考

React+TypeScript+webpackの環境構築

必要なライブラリのインストール

ビルドツールをインストールする

npm install --save-dev webpack webpack-cli webpack-dev-server typescript ts-loader

React関連のライブラリをインストールする。 ビルド後のファイルに含めるので--saveとする。

npm install --save react react-dom @types/react @types/react-dom
  • react-dom: ReactオブジェクトをDOMに反映するライブラリ
  • @types/react: Reactに関する型定義
  • @types/react-dom: react-domに関する型定義

scriptsの追加

${APP_DIR}/package.jsonにscriptsを追加

{
  "scripts": {
    "build:prod": "webpack --mode production",
    "build": "webpack",
    "watch": "webpack --watch",
    "start": "webpack serve"
  },
  # 略
}

TypeScriptの設定を行う

${APP_DIR}/tsconfig.json に以下を書き出す。

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "ES5",    // ECMAScript 5に変換
    "module": "ES2015", // ES Modulesとして出力
    "jsx": "react",     // JSXの書式を有効に設定

    // 非相対パスを指定したとき、インポート対象をnode_modulesから探す
    "moduleResolution": "node", 

    // targetに含まれていないライブラリを組み込む
    "lib": [
      "ES2020",
      "DOM"
    ]
  }
}

webpackの設定を行う

${APP_DIR}/webpack.config.js に以下を書き出す。

const path = require('path')

module.exports = {
  mode: 'development',

  entry: {
    index: path.join(__dirname, '/src/index.tsx'),
  },

  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,          // 拡張子 .ts と .tsx のファイルを
        use: 'ts-loader',         // ts-loaderでトランスパイルする
        exclude: /node_modules/,  // ただし外部ライブラリは除く
      },
    ],
  },

  // 拡張子指定なしでimportできるおまじない
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"]
  },

  // ES5向けの指定(webpack 5以上で必要)
  target: ["web", "es5"],

  devServer: {
    host: '0.0.0.0',
    port: 8080,
  },
};

TypeScriptでReactのコードを書いてみる

${APP_DIR}/src/index.tsx

import * as React from "react";
import * as ReactDOM from "react-dom";
import { ShoppingList } from "./ShoppingList";

let element: JSX.Element = <ShoppingList name="weekend"/>;
let dom: HTMLElement = document.getElementById('root');
ReactDOM.render(element, dom);

${APP_DIR}/src/ShoppingList.tsx

import * as React from "react";

interface Props {
  name: string,
};

interface State {
};

export class ShoppingList extends React.Component<Props, State> {
  render() {
    return (
      <div className="shopping-list">
        <h1>Shopping List for {this.props.name}</h1>
        <ul>
          <li>Instagram</li>
          <li>WhatsApp</li>
          <li>Oculus</li>
        </ul>
      </div>
    );
  }
};

${APP_DIR}/index.html

<!DOCTYPE html>
<html>
<head></head>
<body>
  <div id="root"></div>
  <script type="text/javascript" src="./dist/index.js"></script>
</body>
</html>

ビルドなどをする

# ビルド
npm run build

# 開発サーバ立ち上げる
npm run start

補足

いつの間にか、webpackDevServerの起動方法が変わっていた。 webpack-dev-serverwebpack serve

参考

TypeScriptのコードをJestでテストする

やり方には、ts-jestを使う方法と、babelを使う方法がある。
babelを使うやり方は、https://kinosuke.hatenablog.jp/entry/2020/01/29/115640 と同じアプローチ。preset-envをpreset-typescriptにするだけ。ただし、型チェックはできない。
今回は、型チェックもやってほしいので、ts-jestを使う方法を試してみる。

ライブラリのインストール

npm install --save-dev jest ts-jest @types/jest
  • @types/jest
    • jestの型を定義
  • ts-jest
    • Jest用のTypeScriptプリプロセッサ
    • JestがTypeScriptをトランスパイルできるようになる

Jestを設定する

# 設定ファイルを作成する
npx jest --init

${APP_DIR}/jest.config.js に以下を書き込む。

module.exports = {
  // Jestの検索対象となるパス
  roots: [
    "<rootDir>"
  ],
  // テストコードを書いたファイルを特定するための条件
  "testMatch": [
    "**/__tests__/**/*.+(ts|tsx|js)",
    "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  // ts/tsxファイルに対してts-jestを使うよう設定
  "transform": {
    "^.+\\.(ts|tsx)$": "ts-jest"
  },
}

テストスクリプトを準備する

${APP_DIR}/package.json に以下を追加

{
  "scripts": {
    # 略
    "test": "jest",
    "test:w": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  # 略
}

テストコードを書いてみる

src/Animal.ts にテスト対象コードを書く。

export class Animal {
  animaltype: string;
  name: string;

  constructor(animalType: string, name: string) {
    this.animaltype = animalType;
    this.name = name;
  }

  say(): string {
    let content: string = "`Hello. I am ${this.name} of ${this.animaltype}.`";
    return content;
  }
}

test/Animal.test.ts にテストコードを書く。

import { Animal } from '../src/Animal';

describe('say', () => {
  it("`Hello`を含む文字列を返すこと", () => {
    let animal = new Animal('cat', 'popuko');
    let reg = /Hello/;
    let result = animal.say();
    expect(reg.test(result)).toEqual(true);
  })
})

テストを実行する

# 全部をテストする
npm run test test/.

# ファイル名を指定してテストする
npm run test test/Animal.test.ts

補足

テスト時に以下の警告が出た。
importで問題がある場合は、esModuleInteropを使うとよさそうですね、とのこと。

ts-jest[config] (WARN) message TS151001: If you have issues related to imports, you should consider setting `esModuleInterop` to `true` in your TypeScript configuration file (usually `tsconfig.json`). See https://blogs.msdn.microsoft.com/typescript/2018/01/31/announcing-typescript-2-7/#easier-ecmascript-module-interoperability for more information.

毎回警告が出るのは気になるので、tsconfig.jsonに以下を追加。

{
  "compilerOptions": {
    # 略
    "esModuleInterop": true
  }
}

esModuleInteropって何がいいの?

// esModuleInterop無効の場合
// CommonJSのモジュールは、requireで読み込まないとだめ。
`const moment = require('moment');`

// esModuleInterop有効な場合
// CommonJSのモジュールもES Modulesと同じ書き方で読み込めるようになる!
import moment from 'moment'

参考

TypeScript+webpack環境を作る - ts-loader使う版

https://kinosuke.hatenablog.jp/entry/2021/07/23/165039 のやりなおし。 babelはTypeScriptからJavaScriptへの変換だけをやってくれるだけで、肝心の型チェックはやってくれなかったので、ts-loaderを使ってやってみる。

必要なライブラリをインストール

npm install --save-dev webpack webpack-cli typescript ts-loader
  • webpack:
    • モジュールバンドラー。
    • JavaScript等の複数のファイルを1つにまとめる。
    • loaderを組み込むことで、まとめる前に変換処理(ES2015→ES5など)を加えることができる。
  • webpack-cli:
    • webpackのcli
  • ts-loader:
    • webpackにTypeScriptの変換処理を組み込むloader

${APP_DIR}/package.json の更新

scriptsにビルドコマンドを追加する

{
  "scripts": {
    "watch": "webpack --watch",
    "build": "webpack --mode production"
  },
  "devDependencies": {
    # 略
  }
}

${APP_DIR}/tsconfig.json の作成

TypeScriptをどのように変換するかを設定する

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "ES5", // TSはECMAScript 5に変換
    "module": "ES2015" // TSのモジュールはES Modulesとして出力
  }
}
  • CommonJSのモジュール:
    • require/module.exportsで外部JSファイルを扱う
    • Node.jsが採用している
    • tsconfig.jsonのデフォルト
  • ES Modules:
    • import/exportで外部JSファイルを扱う
    • ES2015が採用している
    • webpackのTree Shaking(未使用のimportを振り落とす)が効く

${APP_DIR}/webpack.config.js の作成

webpackのバンドル設定を行う

const path = require('path')

module.exports = {
  // production:  改行やインデントを削除してJSファイル出力
  // development: ソースマップ有効でJSファイル出力
  // ただし `webpack --mode production` したときは production が有効になる
  mode: 'development',

  // 入力元の指定
  entry: {
    index: path.join(__dirname, '/src/index.ts'),
  },

  // 出力先の指定
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
  },

  module: {
    rules: [
      {
        test: /\.ts$/,            // 拡張子 .ts のファイルを
        use: 'ts-loader',         // ts-loaderでトランスパイルする
        exclude: /node_modules/,  // ただし外部ライブラリは除く
      },
    ],
  },

  // import './MyLib.ts' が 
  // import './MyLib' の書き方でOKになる
  resolve: {
    extensions: [
      '.ts', '.js',
    ],
  },
};

tsファイルを書いてみる

${APP_DIR}/src/index.ts

import { Animal } from './Animal';
let animal = new Animal("cat", "pipimi");
animal.say();

${APP_DIR}/src/Animal.ts

export class Animal {
  animaltype: string;
  name: string;

  constructor(animalType: string, name: string) {
    this.animaltype = animalType;
    this.name = name;
  }

  say() {
    console.log(`Hello. I am ${this.name} of ${this.animaltype}.`);
  }
}

ビルドする

npm run build

参考

TypeScript+webpack環境を作る - babel使う版

TypeScriptをはじめてみる - コピペコードで快適生活 の続き

必要なライブラリをインストール

npm install --save-dev webpack webpack-cli typescript babel-loader @babel/core @babel/preset-typescript
  • webpack:
    • モジュールバンドラー。
    • JavaScript等の複数のファイルを1つにまとめる。
    • loaderを組み込むことで、まとめる前に変換処理(ES2015→ES5など)を加えることができる。
  • webpack-cli:
    • webpackのcli
  • babel-loader:
    • webpackにbabelの変換処理を組み込むloader
    • babelは古いブラウザで動くJavaScriptに変換するツール群
  • @babel/core:
    • babelのコア機能
  • @babel/preset-typescript
    • TypeScript→JavaScriptの変換を行う
    • babelでの変換処理本体は、プラグインとして提供される
    • @babel/preset-env は最新のJavaScriptからの変換を行う

${APP_DIR}/package.json の更新

scriptsにビルドコマンドを追加する

{
  "scripts": {
    "watch": "webpack --watch",
    "build": "webpack --mode production"
  },
  "devDependencies": {
    "@babel/core": "^7.14.8",
    "@babel/preset-typescript": "^7.14.5",
    "babel-loader": "^8.2.2",
    "typescript": "^4.3.5",
    "webpack": "^5.45.1",
    "webpack-cli": "^4.7.2"
  }
}

${APP_DIR}/tsconfig.json の作成

TypeScriptをどのように変換するかを設定する

{
  "compilerOptions": {
    "sourceMap": true,
    "target": "ES5", // TSはECMAScript 5に変換
    "module": "ES2015" // TSのモジュールはES Modulesとして出力
  }
}
  • CommonJSのモジュール:
    • require/module.exportsで外部JSファイルを扱う
    • Node.jsが採用している
    • tsconfig.jsonのデフォルト
  • ES Modules:
    • import/exportで外部JSファイルを扱う
    • ES2015が採用している
    • webpackのTree Shaking(未使用のimportを振り落とす)が効く

${APP_DIR}/webpack.config.js の作成

webpackのバンドル設定を行う

const path = require('path')

module.exports = {
  // production:  改行やインデントを削除してJSファイル出力
  // development: ソースマップ有効でJSファイル出力
  // ただし `webpack --mode production` したときは production が有効になる
  mode: 'development',

  // 入力元の指定
  entry: {
    index: path.join(__dirname, '/src/index.ts'),
  },

  // 出力先の指定
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js',
  },

  module: {
    rules: [
      {
        test: /\.ts$/,            // 拡張子 .ts のファイルを
        loader: 'babel-loader',   // babelでトランスパイルする
        options: {
          presets: ['@babel/preset-typescript']
        },
        exclude: /node_modules/,  // ただし外部ライブラリは除く
      },
    ],
  },

  // import './MyLib.ts' が 
  // import './MyLib' の書き方でOKになる
  resolve: {
    extensions: [
      '.ts', '.js',
    ],
  },
};

tsファイルを書いてみる

${APP_DIR}/src/index.ts

import { Animal } from './Animal';
let animal = new Animal("cat", "pipimi");
animal.say();

${APP_DIR}/src/Animal.ts

export class Animal {
  animaltype: string;
  name: string;

  constructor(animalType: string, name: string) {
    this.animaltype = animalType;
    this.name = name;
  }

  say() {
    console.log(`Hello. I am ${this.name} of ${this.animaltype}.`);
  }
}

ビルドする

npm run build

参考

TypeScriptをはじめてみる

TypeScriptの雰囲気を掴むために、5分でできるチュートリアルをやってみる。

環境の準備

PC環境は汚したくないので、dockerで環境を作る。

#{APP_DIR}/docker-compose.yml に以下を書く

version: '3'
services:
  node:
    image: node
    volumes:
     - "./:/var/app"
    tty: true

コンテナを起動してログインする

docker-compose up
docker-compose exec node /bin/bash

TypeScriptをインストール

cd /var/app
npm install -g typescript

TypeScriptを書いていく

#{APP_DIR}/greeter.ts を書いていく

// クラスの定義
class Student {
    // プロパティの宣言
    fullName: string;

    // コンストラクタ
    // "引数名: 型" で引数の型指定ができる 
    // 引数にpublicをつけることでプロパティとなる
    constructor(public firstName: string, public middleInitial: string, public lastName: string) {
        this.fullName = firstName + " " + middleInitial + " " + lastName;
    }
}

// インターフェースの定義
interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person: Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

// let user = { firstName: "Jane", lastName: "User"};
let user = new Student("Jone", "M.", "User");

document.body.innerHTML = greeter(user);

トランスパイルする

以下のコマンドでトランスパイルできる。 同じディレクトリにjsファイルができる。

tsc greeter.ts

参考:

Kubernetesの設定ファイルの書き方キホン

確認できる環境(minikube)の準備

$ brew install kubectl
$ kubectl version --client

$ brew install minikube
$ minikube version

minikube コマンド

minikube start   # 起動(デフォルトはDockerで起動)
minikube status  # 状態表示
minikube ssh     # ホストOSにログイン
minikube stop    # 停止

※minikubeを起動すると、~/.kube/config がよしなに変更されて、kubectlコマンドでminikube環境への接続が可能になる。

Pod

  • 1個以上のコンテナの集まり
  • 1Podに1コンテナ配置することが多い。
  • 複数のコンテナでストレージを共有するようなケースでは1Podに複数コンテナを配置する
  • Deploymentとセットで定義することが多いが、Pod単体を設定を書くこともできる。
  • https://kubernetes.io/ja/docs/concepts/workloads/pods/pod-overview/

Deployment

Deploymentの設定ファイル

apiVersion: apps/v1      # 固定値
kind: Deployment         # 固定値
metadata:
  name: nginx-deployment # Deployment名
  labels:
    app: nginx           # Podテンプレートと同じ値
spec:
  # レプリカ数
  replicas: 3

  # デプロイメントの履歴数
  # デフォルト値は10
  revisionHistoryLimit: 5

  # このDeployamentが管理するPodをどう見つけるかの定義
  # 基本はPodテンプレートの`labels`と同じにすれば良い
  selector:
    matchLabels:
      app: nginx

  # Podテンプレート
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.7.9
          ports:
            - containerPort: 80 # コンテナが開けるポート

Deploymentの作成・確認・削除

$ kubectl apply -f nginx-deployment.yml        # 設定をもとにオブジェクトを作成する
$ kubectl get deployment                       # オブジェクトの一覧を表示する
$ kubectl describe deployment nginx-deployment # オブジェクトの詳細情報を表示する
$ kubectl delete -f nginx-deployment.yml       # オブジェクトを削除する

Service

Serviceの設定ファイル

kind: Service   # 固定値
apiVersion: v1  # 固定値
metadata:
  # Serviceの名前
  # 他のPodから `http://nginx-service` でアクセス可
  name: nginx-service
spec:
  # 接続タイプ
  # - ClusterIP クラスタ内部のみ接続可能(デフォルト値)
  # - NodePort クラスタ外から接続可能
  type: NodePort

  # どのPodを対象にするか
  selector:
    app: nginx

  # 公開するポート
  ports:
  - name: http       # 複数ポートを指定するときはname必須
    protocol: TCP
    port: 80         # 受け口ポート
    targetPort: 80   # 転送先ポート
  - name: https
    protocol: TCP
    port: 443        # 受け口ポート
    targetPort: 443  # 転送先ポート

Serviceの作成・確認・削除

$ kubectl apply -f nginx-service.yml        # 設定をもとにオブジェクトを作成する
$ kubectl get service                       # オブジェクトの一覧を表示する
$ kubectl describe service nginx-service    # オブジェクトの詳細情報を表示する
$ kubectl delete -f nginx-service.yml       # オブジェクトを削除する

アクセスできるURLの確認

$ minikube service nginx-service --url
http://192.168.64.2:32599

コンテナにログインする

以下のコマンドでPod一覧を表示する。

$ kubectl get pod
NAME                                READY   STATUS    RESTARTS   AGE
nginx-deployment-5d59d67564-2gzp2   1/1     Running   2          2d1h
nginx-deployment-5d59d67564-4mvjs   1/1     Running   2          2d1h
nginx-deployment-5d59d67564-wlglr   1/1     Running   2          2d1h

で、NAME値を指定してkubecltコマンドを実行することで、sshログインができる。

kubectl exec --stdin -tty nginx-deployment-5d59d67564-2gzp2 -- /bin/bas

参考