Angular でフォーム実装してみた

1. Overview

フォームの実装をとおして, Angular (Angular 2 〜) の基礎 (の一部) を学んでみました. その備忘録です.

2. Syntax highlighting of TypeScript by Vim

と … その前に, 私はエディタに Vim を利用しています. Angular では, アプリケーションコードを TypeScript で記述する (Angular 自体も TypeScript で実装されています) ので, TypeScript のシンタックスハイライトを導入します.

プラグインマネージャーは Vundle を利用します.

まずは, 以下のリポジトリを clone します.

$ git clone https://github.com/gmarik/Vundle.vim.git ~/.vim/bundle/Vundle.vim

.vimrc に以下の記述を追加します.

filetype off

set rtp+=~/.vim/bundle/Vundle.vim

call vundle#begin()

Plugin 'gmarik/Vundle.vim'
Plugin 'leafgarland/typescript-vim'

call vundle#end()

filetype plugin on

 

そして, 以下のコマンドを実行します.

vim +PluginInstall +qall

3. Angular CLI

Angular は, クライアントサイドを (あえて) MVC に分類すると, すべてを担う, フルスタックフレームワークです (ちなみに, React は View のみとシンプル). そのため, (個人的主観ではありますが) ファイル構成が複雑で, 環境構築にもコストを要します. アプリケーションを実装するたびにこれらの作業をこなすのは現実的ではありません.

Angular CLI はこの問題を簡単に解決してくれます. Angular CLI を利用することで, コマンド 1 つで, 最低限必要なファイルの作成や環境構, つまり, アプリケーションの雛形生成を自動でおこなってくれます. Amngular CLI を利用するには, npm でインストールします.

$ npm install -g @angular/cli

アプリケーションの雛形を生成するには, ng new を実行します. 今回は, アプリケーション名を form とします.

$ ng new form
$ cd form

 

アプリケーション名でディレクトリが生成されるので, そのディレクトリに移動し, ls でファイルを確認すると, 多くのファイルやディレクトリが生成されていることがわかります.

% ls -A
.editorconfig     README.md         e2e               package-lock.json src               tslint.json
.gitignore        angular.json      node_modules      package.json      tsconfig.json

これは便利ですね.

3. Implement Form

準備ができたのでフォームを実装します. まずは, src/app/app.module.ts に FormsModule を追加します.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

次に, src/app/app.component.ts の AppComponent に, プロパティとメソッドを定義します.

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  private title = 'Form by Angular';
  private user = {
    mail     : 'xxx@gmail.com',
    password : '',
    name     : 'rilakkuma',
    memo     : ''
  };

  show() {
    Object.values(this.user).forEach(value => alert(value));
  }
}

そして, テンプレートである, app.component.html を以下のように記述します.

<form #myForm="ngForm" (ngSubmit)="show()" novalidate>
   <div>
     <label>メールアドレス : <input type="email" name="mail" [(ngModel)]="user.mail" #mail="ngModel" required email /></label>
     <span *ngIf="mail.errors?.required">メールアドレスは必須です</span>
     <span *ngIf="mail.errors?.email">メールアドレスを正しい形式で入力してください</span>
   </div>
   <div>
     <label>パスワード : <input type="password" name="password" [(ngModel)]="user.password" #password="ngModel" required minlength="6"></label>
     <span *ngIf="password.errors?.required">パスワードは必須です</span>
     <span *ngIf="password.errors?.minlength">パスワードは 6 文字以上で入力してください</span>
   </div>
   <div>
     <label>名前 : <input type="text" name="name" [(ngModel)]="user.name" #name="ngModel" required minlength="3" maxlength="10" /></label>
     <span *ngIf="name.errors?.required">名前は必須です</span>
     <span *ngIf="name.errors?.minlength">名前は 3 文字以上で入力してください</span>
     <span *ngIf="name.errors?.maxlength">名前は 18 文字以上で入力してください</span>
   </div>
   <div>
     <label>備考 : <textarea name="memo" [(ngModel)]="user.memo" #memo="ngModel" maxlength="10"></textarea></label>
     <span *ngIf="name.errors?.maxlength">備考は 10 文字以内で入力してください</span>
   </div>
   <div>
     <button type="submit" [disabled]="myForm.invalid">送信</button>
   </div>
</form>

詳細を解説していきます.

まず, form タグには, それぞれ ngFrom ディレクティブ, ngSubmit イベントバインディング, novalidate 属性 (ブラウザによるネイティブの検証を無効にする属性です. Angular 4 〜 では, 自動で付与してくれるので不要です) を指定しています.

<form #myForm="ngForm" (ngSubmit)="show()" novalidate>

ngForm ディレクティブは, submit 時に, フォーム全体の検証をするために利用します.

<button type="submit" [disabled]="myForm.invalid">送信</button>

API が変わっているだけで, AngularJS での, ng-form, ng-submit と大差ないことに気づかれたでしょうか … ?

入力フォーム (email, password, text, textarea) です.

<label>メールアドレス : <input type="email" name="mail" [(ngModel)]="user.mail" #mail="ngModel" required email /></label>

Angular でのフォーム検証を有効にするためは, 双方向バインディング必要となるので, ngModel ディレクティブと要素を識別するための, name 属性が必須になります.

[(ngModel)]="user.mail"

では, AppComponent#user プロパティと双方向バインディングしています. また,

#mail="ngModel"

は, テンプレート参照変数で, ngModel ディレクティブを代入することで, フォーム要素の状態にアクセスできるようにします.

required, email, minlength, maxlength などは, 必要に応じて指定します.

そして, 不正な値であった場合のエラー表示に活躍するのが, *ngIf ディレクティブです (AngularJS での ng-if ディレクティブ).

<span *ngIf="mail.errors?.required">メールアドレスは必須です</span>

*ngIf ディレクティブは, その値が true であった場合のみ, 指定した要素を DOM に追加します. *ngIf ディレクティブと, フォームの検証の成否を組み合わせることで, 簡単にエラーメッセージの表示を実装することが可能です.

フォームの検証の成否の構文を一般化すると,

入力要素名 (テンプレート参照変数名).errors?.検証型

となります. ? は, Angular 2 〜 で利用可能な, Safe Navigation Operator で, errors が null や undefined であってもエラーを発生させることがありません (つまり, 安全にプロパティアクセス可能になります). 検証型には, フォームに指定した, required, email, minlength, maxlength などを指定します.

あとは, フォームに不正な値が入っていれば, submit ボタンが disabled となるように実装します.

<button type="submit" [disabled]="myForm.invalid">送信</button>

[disabled] は, プロパティバインティングです. そこに, (フォームを参照するテンプレート変数).invalid とすることで, それが実装できます. ここでは, myForm が, フォームを参照するテンプレート変数となるので, myForm.invalid と指定します.

最後に,

$ npm start

を実行することで, ng serve が実行され, 簡易サーバーが起動するので, localhost:4200 にアクセスして, フォームが機能していれば完成です !

Web Audio Library XSound 2.4.0 / 2.4.1 Released

1. Overview

久々の Web Audio API ネタです. XSound 2.4.0 からノイズの生成に対応しました. 2.4.0 ではホワイトノイズ (白色雑音), 2.4.1 ではピンクノイズの生成が可能になりました.

2. What is Noise ?

そもそもノイズ (雑音) とは何か ? から解説します. 世の中に存在する音のほとんどは周期的な波形をしており, 基本周波数をもつ sin 波とその整数倍の周波数をもつ sin 波 (倍音) の合成によって構成されます (それを周波数視点で分解するのがフーリエ変換です). しかしながら, 非周期的な波形をもつ音も存在しており, その典型例がノイズと呼ばれる音になります. ノイズにはいくつか種類がありますが, 有名でよく利用されるホワイトノイズとピンクノイズの特徴と, それを Web Audio API で生成するための実装を紹介します.

3. White Noise

ホワイトノイズ (白色雑音) の命名の由来は, そのスペクトルが白色のスペクトルと同じ (どの帯域でも同じ振幅をとる) であることから名づけられました. ホワイトノイズの実装はとても簡単です. なぜなら, 乱数を生成するだけだからです (ただし, 振幅が -1 〜 1 に収まるように値を調整する必要はあります). JavaScript の Math.random メソッドは 0 以上 1 未満の値を返すので, -0.5 したあと, 2 倍にすることで, -1 〜 1 の値に収まるようにしています.

// processor は ScriptProcessorNode のインスタンス
processor.onaudioprocess = event => {
    const outputLs = event.outputBuffer.getChannelData(0);
    const outputRs = event.outputBuffer.getChannelData(1);

    // bufferSize は ScriptProcessorNode のバッファサイズ
    for (let i = 0; i < bufferSize; i++) {
        outputLs[i] = 2 * (Math.random() - 0.5);
        outputRs[i] = 2 * (Math.random() - 0.5);
    }
}

4. Pink Noise

ピンクノイズの命名の由来は, そのスペクトルがピンク色のスペクトルと同じ (高周波数ほど振幅が減衰する) であることから名づけられました. ピンクノイズの実装は少し複雑 (すみませんが, 私も完全に理解できていません …) なので, 実装のみを紹介します.

// processor は ScriptProcessorNode のインスタンス
processor.onaudioprocess = event => {
    const outputLs = event.outputBuffer.getChannelData(0);
    const outputRs = event.outputBuffer.getChannelData(1);

    let b0 = 0;
    let b1 = 0;
    let b2 = 0;
    let b3 = 0;
    let b4 = 0;
    let b5 = 0;
    let b6 = 0;

    // bufferSize は ScriptProcessorNode のバッファサイズ
    for (let i = 0; i < bufferSize; i++) {
        const white = (Math.random() * 2) - 1;

        b0 = (0.99886 * b0) + (white * 0.0555179);
        b1 = (0.99332 * b1) + (white * 0.0750759);
        b2 = (0.96900 * b2) + (white * 0.1538520);
        b3 = (0.86650 * b3) + (white * 0.3104856);
        b4 = (0.55000 * b4) + (white * 0.5329522);
        b5 = (-0.7616 * b5) - (white * 0.0168980);

        outputLs[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + (white * 0.5362);
        outputRs[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + (white * 0.5362);

        outputLs[i] *= 0.11;
        outputRs[i] *= 0.11;

        b6 = white * 0.115926;
    }
}

う〜ん … 原理がよくわかりません w. 実は, ホワイトノイズに適切な Low-Pass Filter をかけることでも生成できるので, 原理がわからないと使いたくない場合は, その実装でもいいかもしれません.

5. References

Web Components <x-piano> v2.0.0 をリリースしました

1. Overview

Web Components である, <x-piano> v2.0.0 をリリースしました. v2.0.0 では, 以下の API で構成されています.

  • Shadow DOM v1
  • Custom Elements v1
  • ESModules

* Web Components を構成する API には HTML Templates も含まれますが, <x-piano> では利用していないので, 割愛します.

ちなみに, v1.x.x までは以下の API で構成されていました.

  • Shadow DOM v0
  • Custom Elements v0
  • HTML Imports

詳細な仕様は, Web Fundamentals などに記載されていますので, <x-piano> v2.0.0 のコードを例に, それぞれの API がどのようになっているかを簡単に解説します.

2. Shadow DOM v1

Shadow DOM とは, ツールや命名規則がなくても, Vanilla JavaScript で CSS とマークアップをバンドルし, 実装の詳細を非表示にして, 自己完結型 (例えば, document.querySelector は, コンポーネントの Shadow DOM 内のノードを返しません) のコンポーネントを作成するための API です. 詳細は, 最後のセクションのリファレンスを参考にしてください.

Shadow DOM v0 では, createShadowRoot メソッドを利用して, Shadow Root を生成していましたが, v1 では, 以下のようなコードになります (<x-piano> v2.0.0 より).

export default class Piano extends HTMLElement {
    /** @override */
    constructor() {
        super();

        this.attachShadow({ mode : 'open' });
    }

    render() {
        this.shadowRoot.innerHTML = `...`
    }
}

まず, HTMMLElement を継承したクラスを実装します. そして, attachShadow メソッドに, 引数 { mode : ‘open’ } を指定することで, Shadow Root が生成されます. あとは, Shadow DOM を生成するために innerHTML (や, appendChild) などの DOM 操作のメソッドを利用します.

3. Custom Elements v1

Custom Elements とは, その名前のとおり, 新しい HTML タグを作成したり, 既存の HTML タグを拡張したりするための API です. 詳細は, 最後のセクションのリファレンスを参考にしてください.

Custom Elements v0 では, document.registerElement というメソッドを利用して, Custome Elements を定義していましたが, Custom Elements v1 では, 以下のようなコードになります (<x-piano> v2.0.0 より).

export default class Piano extends HTMLElement {
    static get observedAttributes() {
        return [
            'ui-only',
            'type',
            'volume',
            'transpose',
            'glide',
            'attack',
            'decay',
            'sustain',
            'release'
        ];
    }

    constructor() {
        // 要素のインスタンスが作成またはアップグレードされたとき. 状態の初期化, イベントリスナーの設定, または, Shadow DOM の作成に利用します.
    }

    connectedCallback() {
        // 要素が DOM に挿入されるたびに呼び出されます. リソースの取得やレンダリングなどの, セットアップコードの実行に利用します.
    }

    disconnectedCallback() {
        // 要素が DOM から削除されるたびに呼び出されます. クリーンアップ コードの実行 (イベント リスナーの削除など) に利用します.
    }

    attributeChangedCallback() {
        // 属性が追加, 削除, 更新, または, 置換されたときに呼び出されます. また, そのためには, 対象の属性を指定する static get observedAttributes メソッドの実装が必要です.
    }
}

window.customElements.define('x-piano', Piano);

まずは, 拡張したい HTMLElement (例えば, HTMLButtonElement など) を指定して, クラスを実装します. あとは, window.customElements.define メソッドを利用して, タグ名を第 1 引数に, 対象のクラスを第 2 引数に指定して, Custom Elements を定義するだけです. これだけで, 最低限の実装は完了です. あとは, 必要に応じて, constructor や callback となるメソッドを実装するだけです.

4. ESModules

旧仕様の Web Components では, Custom Elements などを定義したファイルを読み込むには, HTML Imports という API を利用していましたが, 現在の仕様では, ESModules を利用するようになっています. API は非常に簡単で, script タグの type 属性に module を指定するだけです. また, ESModules に対応していないブラウザのために, nomodule 属性を利用した script タグも用意して, webpack などでバンドルしたフォールバックのスクリプトを読み込ませておきます. 具体的なコードは, 以下のようになります (<x-piano> v2.0.0 より).
定義した Custom Elements などを利用したい HTML のファイルで,

<script type="module" src="src/components/index.js"></script>
 <script nomodule src="build/app.js"></script>

のように, script タグを記述します.

5. References

4 年ぶりにメインサイトをリニューアルしました (レスポンシブ対応)

1. Overview

メディアクエリを利用せずにレスポンシブ対応をしました. その場合, 重要となるのが以下の 2 つです.

  • calc
  • max-width

では具体的に, どのように重要となるのか ? 各ページの対応ともに解説します.

2. Implement

2 – 1. Header, Profile, Skills, Footer

これらのページでは, テキストを中心に構成されているので, レスポンシブ対応をするためには, font-size をデバイス幅に応じて変更する必要がありました. メディアクエリを利用せずにこれを実現するには, calc を利用して, font-size を以下のように設定します. 以下の設定では, max-width: 1280px, min-width: 320px, 最小の font-size を 12px, 最大の font-size を 16px とした場合の値です.

font-size: calc(4 * ((100vw - 320px) / (1280 - 320)) + 12px);

例えば, iPhone 5 のような, 320px 幅のデバイスの場合, 100vw = 320px となるので, font-size は 12px となります. 逆に, デスクトップで, 1280px 以上あるような場合, 100vw = 1280px となるので, font-size は 16px となります. つまり, 4 というのは, 変化量で, 真ん中の () でくくられた算出式は, max-width と min-width から決定され, 最後の 12px というのは, 最小の font-size になるわけです. 一般化すると,

font-size: calc({変化量} * ((100vw - {min-width}) / ({max-width} - {min-width})) + {最小の font-size});

これを, html などに指定し, rem や em で font-size を指定すれば, メディアクエリを利用せずに, レスポンシブな font-size を実装できます.

2 – 2. Portfolio

ポートフォリオページでは, カルーセルパネルのレスポンシブ対応が必要でした. まず, カルーセルパネルのコンテナとなる要素 (サイトでは, .Portfolio__carousel) に, 必要な max-width を指定します. また, カルーセルパネルに入る個々の画像 (img タグ) には, max-width (デスクトップでの表示幅) と width: 100vw を指定します. CSS の設定はこれだけです.

次に, HTML タグ (JSX) と JavaScript の処理です. img タグの width / height 属性指定しないようにします (CSS の width: 100vw によって, 比率を保ったままレスポンシブになります). そして, アニメーションさせるためのオフセット値を, img 要素の width から算出するようにします. ただし, これには欠点があり, 初回の render 時に表示がずれてしまうことがあります.

const offset           = this.image ? this.image.width : 400;  // 初回は 400 (デスクトップと決め打ち)
const slideAmountRight = (slide * -offset) + offset;
const slideAmountLeft  = slide * offset;
const style            = {
    transform : `translateX(${slideAmountRight}px)`,
    left      : `${slideAmountLeft}px`
};

この問題の解決は, issue としておきたいと思います.

2 – 3. Music

ミュージックページでは, Grid Layout を採用していましたが, そもそもコンテンツが多すぎたので, スマートフォンのサイズになると, すべてのコンテンツが縦 (height: 100vh) におさまりきらないという問題がありました. そこで, モバイルファーストの考えに基づいて, コンテンツ (動画) を 2 つに絞りました. さいわい, YouTube は, 動画終了後に関連動画が表示されるのでそれで十分という判断でもありました. デスクトップでは, 横に並び, スマートフォンなどでは, 段落ちして縦に並ぶように実装しました. そのさいに, 動画のサイズもレスポンシブになるように, エラスティックビデオの実装もしました.

.Music__grid {
    /* デスクトップでは, 横に並び, スマートフォンなどでは, 段落ちして縦に並ぶようにするための Grid Layout の設定 */
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(288px, 1fr));
    grid-auto-rows: 1fr;
    grid-gap: 0;
}

/* 以下は, エラスティックビデオの実装に必要なスタイル */
.Music__grid > li {
    display: block;
    position: relative;
    padding-bottom: 56.25%;  /* 315 / 560 */
    height: 0;
    overflow: hidden;
}

.Music__grid > li > iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

2 – 4. ロゴの Retina 対応 + レスポンシブ対応

一般的に, 画像をレスポンシブ対応させるには, width / height 属性を指定せず, CSS で max-width: 100% を指定すればいいのですが, ロゴのように Retina ディスプレイでもきれいに見せたい場合, それだと意図した 2 倍のサイズで表示されてしまいます. しかし, 対応は簡単で, img タグの width 属性だけをオリジナルの画像の width の 1 / 2 に設定すれば, Retina 対応ができ, かつ, max-width: 100% によってレスポンシブ対応にもなります

3. Conclusion

メディアクエリを利用しないレスポンシブ対応の目的は, CSS の実装を複雑にしないことだと思います. しかし, 目的を忘れ, メディアクエリを利用しないことに固執し, 結果, CSS が複雑になってしまっては, 本末転倒です. ディアクエリを利用することは悪ではないので, 必要と判断したならば利用すべきだと思います (今回は, 私自身のトレーニングとして, CSS が複雑になってもメディアクエリを利用せずにチャレンジするというのが目的でしたので). もっとも, 今回実装をしてみて, よほどレイアウトが変わらない限り, たいていのケースでは calc と max-width でのりきれるのではないかと思いました !

4 年ぶりにメインサイトをリニューアルしました (デスクトップ版)

1. Overview

4 年ぶりにメインサイトをリニューアルしました (デスクトップ版のみ. レスポンシブ対応は今後のタスク). この 4 年間にフロントエンドをとりまく環境が大きく変わったことや, 自身のスキルもレベルアップしていることを同時に実感しました. そこで, 開発環境やリニューアルにおいて, 採用した技術などをまとめておきます.

1 – 1. Before renewal

https://weblike-curtaincall.ssl-lolipop.jp/

リニューアル前のサイトです (.htaccess で 301 リダイレクトをしているのでもう閲覧はできません). 制作したのは, 4 年前なので 2014 年です. フラットデザイン + レスポンシブデザインが流行していた頃ですね. したがって, 旧サイトもできるだけ, 画像はもちろん, CSS による装飾も少なくし, かつ, デスクトップ・タブレット, そして, スマートフォンとデバイスに応じて最適なレイアウトに変わるようになっています. 以下, 開発環境と採用した技術です.

1 – 1 – 1. Development

  • OS X のビルトイン Apache

1 – 1 – 2. Technologies

  • HTML, CSS (SCSS), JavaScript
  • jQuery, AngularJS
  • Flash, ActionScript 3.0
  • フラットデザイン, レスポンシブデザイン, アイコンフォント

jQuery, AngularJS, Flash … 時代を感じますね …

1 – 2. After renewal

https://korilakkuma.github.io/LIFE-with-FAVORITES/

リニューアル後のサイトです. ロリポップのサーバーから, GitHub Pages に移行したので, .htaccess で 301 リダイレクトをかけています. 今回のリニューアルにあたってチャレンジしたいこと 2 つありました.

  • 画面幅いっぱいに利用した, HTML 版プレゼンテーションスライドのようなレイアウト
  • メディアクエリを利用しない (極力利用しない) レスポンシブデザイン

特に, 「メディアクエリを利用しない (極力利用しない) レスポンシブデザイン」を実現するために, 旧サイトと比較して, かなりの無駄なコンテンツを削ぎ落としました. 以下, その例です.

  • トップページのお知らせ
  • プロフィールを簡素化
  • ポートフォリオはスクリーンショットのみ, かつ, カルーセルパネルを利用して, 狭い面積でできるだけ多くの情報を伝える
  • ポートフォリオと同様に, YouTube の埋め込み動画も Grid Layout を利用して, 簡潔に情報を伝える
  • 掲示板は, いったん廃止

1 – 2 – 1. Development

  • Babel
  • ESLint
  • stylelint
  • webpack + npm scripts
  • Docker

1 – 2 – 2. Technologies

  • HTML, CSS (PostCSS), JavaScript (ES2015 ~)
  • React
  • アクセシビリティ (WAI-ARIA 属性, role 属性)
  • レスポンシブデザイン (予定)

う〜ん … 時の流れを感じますね … 詳細は, リポジトリを参照してください.

2. Implement

それでは, 各ページ (ヘッダー, プロフィール, ポートフォリオ, スキル, ミュージック, フッター) ごとに簡単に実装を紹介します.

2 – 1. Header

ロゴとサブタイトルのみのコンテンツにしました. また, mouseenter すると, アニメーションが再開されるようになっています. React の処理も, state の操作ぐらいで特に難しいことはしていません. 唯一, ハマったことは, 最初にこのページを実装したのですが, width: 100vw; にしてしまうと, スクロールバーの横幅に依存しないので, 横スクロールバーが表示されてしまうことでした. したがって, 以下のようにして, ページを画面いっぱいに広げました

selector {
  width: 100%;
  height: 100vh;
}

2 – 2. Profile

ここも, レスポンシブ対応を意識して, コンテンツを削ぎ落としたぐらいで, 実装では特に難しいことはしていません.

2 – 3. Portfolio

このページのカルーセルパネルがもっとも苦労しました. jQuery で実装していたときは, アニメーションも jQuery (animate メソッド) が担うので比較的簡単でしたが, React になると, アニメーションは CSS に分離させるので, jQuery でカルーセルパネルを実装する場合の考えと, React + CSS でカルーセルパネルを実装する場合の考えが根底から異なるのが原因でした.

ループしないタイプのカルーセルパネルはなんとか自力で実装できたのですが, ループするタイプは実装がおよばなかったので, 以下のカルーセルパネルを参考にしました.

ポイントは, Flexbox の order を適切に入れ替えているところでしょうか. また, transition の duration は, アニメーションの質を損なわない範囲で, できるだけ短く設定するのがポイントでした. リニューアルサイトでは transition: transform 0.3s ease-out; で設定しています. これはなぜかと言うと, あまり長く設定すると, order を入れ替えたときに, サイドのパネルが消えるのがもろに見えてしまうからです. 短くすることで, 人間の感覚上, 消えてないように見せかけています.

2 – 4. Skills

ここも, レスポンシブ対応を意識して, コンテンツを削ぎ落としたぐらいで, 実装では特に難しいことはしていません.

2 – 5. Music

CSS の比較的新しいスタイルである, Grid Layout を導入しました (もしかしたら, Edge はダメかも …). 最も見てもらいたい動画 (Forever Love (Piano Instruments)) を左の最も面積の広い部分に配置し, 残りをグリッド (格子状) に配置しています. なんだか仕様が複雑そう (Flexbox のような印象をもっていました …) と思いきや, 実装してみると, 意外とシンプルでした.

.Music__grid {
    margin-top: 12px;
    display: grid;
    grid-template-columns: 560px 280px 1fr;
    grid-template-rows: 158px 1fr;
}

.Music__grid > li {
    display: block;
}

.Music__grid > li:first-child {
    grid-column-start: 1;
    grid-column-end: 2;
    grid-row-start: 1;
    grid-row-end: auto;
}

.Music__grid > li:nth-child(2) {
    grid-column-start: 2;
    grid-column-end: 3;
    grid-row-start: 1;
    grid-row-end: 2;
}

.Music__grid > li:nth-child(3) {
    grid-column-start: 3;
    grid-column-end: 4;
    grid-row-start: 1;
    grid-row-end: 2;
}

.Music__grid > li:nth-child(4) {
    grid-column-start: 2;
    grid-column-end: 3;
    grid-row-start: 2;
    grid-row-end: 3;
}

.Music__grid > li:nth-child(5) {
    grid-column-start: 3;
    grid-column-end: 4;
    grid-row-start: 2;
    grid-row-end: 3;
}

2 – 6. Footer

お問い合わせフォームがメインです. 旧サイトでは, AngularJS で実装していました. ここも, React の state 管理をちゃんと実装するぐらいで特に難しいことはしていません.

3. Future Tasks

今後のタスクとしては, レスポンシブ対応です. 特に, メディアクエリを使わない (or 極力使わない) ように実装したいです (メディアクエリは, CSS を複雑にしてまうので). その場合, おそらく困難になるのが, ポートフォリオページのカルーセルパネルと, ミュージックページの Grid Layout でしょう.

4. Conclusion

長らく手をつけていないプロダクトをリニューアルすることは, 最新技術の習得にもなりますし, なにより時代の流れを感じて楽しいです !

Go合宿2018 に参加してきました

1. Overview

昨日, 今日とGo合宿2018 に参加してきたのでその成果と感想を記載しておきます.

1 – 1. Targets

合宿に参加した目的は, 大きなところでは, 以下の 3 つとなります.

  • X Sound の WebSocket Server を Node.js から Go に変えると, どのくらいパフォーマンスが向上するか実験してみたい. そして, 短期間で集中的にその実装をしたい
  • 合宿を機会に, 仕事であれ, 趣味であれ, 日常的に Go を利用するようにしたい
  • 普段は, フロントエンドよりの勉強会や LT の参加ばかりなので, サーバーサイドやバックエンド系のイベントにも参加してみたい

1 – 2. Career of Go

2 年ほど前に, 今の会社 (サーバーサイドは Go を利用しているとうかがったので) に転職するのを機会に準備として 1 ヶ月ほど独学で学んだ程度です. また, 肩書きは, フロントエンドエンジニアなので, 日常的に Go は利用しておらず, 2 週間に一度, 事業の制度としてある FIL という日に Go を少し業務で利用するぐらいでした.

2. Development

先に開発した, 最終成果であるリポジトリを紹介しておきます.

package main

import (
	"fmt"
	"golang.org/x/net/websocket"
	"net/http"
	"os"
	"strconv"
)

func main() {
	if len(os.Args) != 2 {
		println("Require port number !")
		os.Exit(1)
	}

	port, err := strconv.ParseUint(os.Args[1], 10, 16)

	if err != nil {
		println("Invalid port number !")
		os.Exit(1)
	}

	var websockets []websocket.Conn

	http.Handle("/", websocket.Handler(func(ws *websocket.Conn) {
		var in []byte

		websockets = append(websockets, *ws)

		for {
			if err = websocket.Message.Receive(ws, &in); err != nil {
				println(err)
				break
			}

			for _, v := range websockets {
				if v != *ws {
					websocket.Message.Send(&v, in)
				}
			}
		}
	}))

	err = http.ListenAndServe(fmt.Sprintf(":%d", port), nil)

	if err != nil {
		panic(err)
	}
}

実質は, 50 行のコードです (w).

ポイントや, つまづいた点を解説します.

まず, WebSocket のパッケージには golang.org/x/net/websocket を利用しました. Go にも様々な WebSocket のパッケージがあるようなのですが, 選定する利用 (条件) として以下の 2 つがありました.

  • バイナリメッセージングが可能である
  • クライアントサイドの実装には影響しない

後者が少しわかりづらいと思うので, もう少し詳細を解説します. WebSocket による処理は, 当然, サーバーサイドだけでなく, クライアントサイドも関わってくるわけですが, クライアントサイドの WebSocket 処理は, すでに XSound の Session クラスが担っています. したがって, JavaScript の WebSocket ライブラリの socket.io のようにクライアントサイドも変更しないといけないとなると, XSound も実装を変更し, また, XSound の思想の 1 つである, 特定のライブラリやフレームワークに依存しないということも破綻してしまうので, 非常に重要な理由でありました.

つまづいた点としては,

for _, v := range websockets {
	if v != *ws {
		websocket.Message.Send(&v, in)
	}
}

の部分で, バイナリメッセージング (X Sound のセッション機能) において, 自身のソケットに送信しては, 音が二重に鳴ってしまうので, 自身のソケットでないかどうかを判定する必要があるのですが, Go のポインタをゴリゴリしたことない私にとっては意外とつまづきました …

3. Future tasks

実装した Go の WebSocket Server を Heroku にデプロイまではできたのですが, どうしても TLS handshake error が解決できず, Heroku の Node.js とローカルでの Go での比較しかできず, 厳密ではありませんが, Go のほうがやはり体感的にパフォーマンスは向上した (音がよくなった) 気がします w

とある方の感想です w

ちなみに, X Sound は https で動作しているので, WebSocket も wss を利用する必要がありました. したがって, Go のコードも以下のように変更して Heroku にデプロイしました …

package main

import (
	"fmt"
	"golang.org/x/net/websocket"
	"net/http"
	"os"
	"strconv"
)

func main() {
	port, err := strconv.ParseUint(os.Getenv("PORT"), 10, 16)

	if err != nil {
		println("Invalid port number !")
		os.Exit(1)
	}

	fmt.Printf("PORT: %d\n", port)

	var websockets []websocket.Conn

	http.Handle("/", websocket.Handler(func(ws *websocket.Conn) {
		var in []byte

		websockets = append(websockets, *ws)

		for {
			if err := websocket.Message.Receive(ws, &in); err != nil {
				println(err)
				break
			}

			for _, v := range websockets {
				if v != *ws {
					websocket.Message.Send(&v, in)
				}
			}
		}
	}))

	http.ListenAndServeTLS(fmt.Sprintf(":%d", port + 0), "server.crt", "server.key", nil)
        http.ListenAndServeTLS(fmt.Sprintf(":%d", port + 1), "server.crt", "server.key", nil)
	http.ListenAndServeTLS(fmt.Sprintf(":%d", port + 2), "server.crt", "server.key", nil)
	http.ListenAndServeTLS(fmt.Sprintf(":%d", port + 3), "server.crt", "server.key", nil)
}

これは, 近日中に解決して, なんとか Go の WebSocket Server で X Sound のセッション機能を楽しんでもらいたいです !

4. Conclusion

ローカルでしか動作をさせることができませんでしたが, Go のバイナリメッセージングの WebSocket Server を実装して, デモをするという最低限の目標は達成できました. また, メルカリの方から DDD の本質をとてもわかりやすくプレゼンしていただき, 思わぬ収穫となりました. 業務とコードの乖離をできるだけ少なくする, また, DDD は銀の弾丸ではなく向き不向きがあることなどなど … そして, 旅館も, また, 旅館の従業員の方々も非常によくて, なかなかの開発環境でした. おそらく, 来年も開催されると思いますので, ぜひ参加したいですね. そのときには, もっともっとレベルアップしてよりよりプロダクトを実装できるようになっていたいです !

MacBook Pro 2017

1. Overview

MacBook Pro 2017 を購入して約 1 ヶ月が経過したので, その使用感を書いておきたくなりました.
ちなみに, 2 台目の Mac で, MacBook Pro を購入するのは初めてです. 以前は, つまり, 最初の Mac は, MacBook Air で 2014 年 3 月 9 日から, 2018 年 5 月 26 日まで, 約 4 年間お世話になりました. 性能や使用感は悪くなかったのですが, SSD 128 GB と容量がかなりきつくなっていたのと, あとは, バッテリーの減りがかなり早くなってきたことが買い替えるきっかけでした.

2 台目の Mac, つまり, MacBook Pro 2017 は, 13 インチのスペースグレイ, Vim を使うので Touch Bar はなし, SSD 256 GB にしました.

2. Feeling of use

2 – 1. CPU

さすがに, MacBook Pro だけあって処理速度は体感できるほど違います. 例えば, Web Audio API で比較的長い楽曲をデコードするのも MacBook Air ではそれなりに時間を要していたのが, 一瞬です.

2 – 2. Display

MacBook Air は Redina ディスプレイではなかったせいか, Retina ディスプレイの MacBook Pro は, 美しい …

2 – 3. Keyboard

MacBook Pro 2017 に買い替える際に, 最も懸念していたのが, バタフライキーボードでした. 最初は, タッチが浅いことに違和感を覚えるかもしれないですが, 慣れると断然使いやすいです. ただ, タッチの音が大きいのは少し気になりますが …

2 – 4. Appearance

はじめは, シルバーを購入しようと思っていたのですが, 売り場でスペースグレイを見て, スペースグレイに即決しました. ずっともっていたくなるようなかっこよさと美しさを兼ね備えています.

3. Built in Apache

これは, MacBook Pro 2017 の問題ではなく, macOS High Sierra の問題なのですが, ビルトインの Apache の設定を少し変える必要がありました (ちょっとしたサンプルコードを動かすときなどは, ビルトインの Apache で動作させるのでよく使っています …).

まずは, Apache の設定です.

$ sudo vim /etc/apache2/httpd.conf

173 – 176 行目がコメントアウトされていたら, コメントを外します

LoadModule userdir_module libexec/apache2/mod_userdir.so
LoadModule alias_module libexec/apache2/mod_alias.so
LoadModule rewrite_module libexec/apache2/mod_rewrite.so
LoadModule php7_module libexec/apache2/libphp7.so

510 行目がコメントアウトされていたら, コメントを外します

Include /private/etc/apache2/extra/httpd-userdir.conf

544 行目がコメントアウトされていたら, コメントを外します

Include /private/etc/apache2/users/*.conf

次に, ユーザーごとの設定です.

$ vim /etc/apache2/users/{ユーザー名}.conf
<Directory /Users/{ユーザー名}/Sites>                                                                                             
    Options Indexes MultiViews FollowSymLinks Includes
    AllowOverride All
    Require all granted
    Order allow,deny
    Allow from all
</Directory>

最後に, Apache を起動します.

$ sudo /usr/sbin/apachectl start

これでブラウザから, http://localhost/~{ユーザー名}/ でアクセスできるようになります.

最後に … High Sierra では, キーチェーンが SSH のパスフレーズを憶えてくれず, 毎回入力しなくてはいけなくてはいけなくめんどくさいので,

$ vim ~/.ssh/config

を作成して,

Host *
  UseKeychain yes
  AddKeysToAgent yes

と入力しておけば, 毎回入力する手間が省けます.

初めて執筆をしました

この度, ご縁があって「WEB+DB PRESS Vol.105」の Firebase 特集の 第 2 章を執筆させていただいたので, その感想などを残しておきます.

きっかけ

きっかけは所属する会社からの経由で, 技術評論社様より Firebase 特集の執筆依頼を受けたので, 過去に Firebase に関連するディベロッパーズブログを公開したというだけで執筆候補に選ばれました. そして, あまり深く考えず, 滅多にない機会だからという理由で引き受けました. 私は, この依頼を受けるまで, 技術雑誌や書籍は, その技術のスペシャリストであったり, 有名であったり, フォロワー数の多いエンジニアが執筆するものだと思っていました (もちろん, そういう方のほうが, 依頼の確率は高いでしょう). 私は, 別に Firebase に関して詳しいわけではなく, ちょこっと業務で使ったというレベルでした. したがって, 執筆の依頼をいただけるかは, 正直, 運や縁なのかなぁーと今回の執筆を振り返って思いました.

執筆業

以下は, 実際に執筆してみて大変だったことです.

  • Firebase 初心者が対象だったので, その視点で執筆する
  • 雑誌なので, 分量調整が意外と大変
  • ちょっと長いコードになるとすぐに改行しないといけない
  • 2 人で執筆したので, 説明の粒度や文章のクセなどを統一する必要があった
  • 通常業務追い込みと執筆の追い込みが重なって死にそうな週があった

… などでしょうか. 私は, ニート時代に WEB SOUNDER という Web Audio API の解説サイトを制作して意外と好評を得た経験があるのですが, Web はその性質上, 分量調整など細かいことに気を配る必要はあまりないですが, やはり, 雑誌という媒体上, 分量調整は想定してた以上に大変でした.

結論

正直, 報酬だけを目当てにするならやらないほうがいいと思います (笑). そうではなく, 1 つのアウトプットの機会ととらえたり, そのアウトプットをより多くの人に読んだいただいたり, また, 執筆をとおして, 業務では会うことのない方と仕事をしたり … そういったことに醍醐味があるような気がします. また, 執筆をしたいかと問われたら, 答えは YES です (できれば, Web Audio API の書籍がいいですね) !

明日, 2018 年 6 月 23 日 (土) 発売です. 少しでも多くの人が, Firebase にふれる機会になればと思います.

webpack 3 から webpack 4 へアップデートする際にハマりそうなこと

1. Overview

なにかと破壊的な変更が多い webpack ですが, v3 から v4 にアップデートする際にも破壊的な変更があるので, 自分の備忘録として残しておきます.

2. webpack-cli

webpack 4 からは, コマンドライン機能が webpack-cli に分離されました. したがって, npm install する際に以下のようにする必要があります.

$ npm install --save-dev webpack webpack-cli

この対応は, あっという間に完了するかと思います.

3. mini-css-extract-plugin

以下の, webpack.config.js は v3 であれば正常に動作するものです.

const webpack            = require('webpack');
const ExtracktTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: ['./src/main.js', './src/main.css'],
  output: {
    filename: 'app.js',
    path: `${__dirname}`
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          ExtracktTextPlugin.extract({
            use: [
              'css-loader',
              'postcss-loader'
            ]
          })
        ]
      },
      {
        test: /\.png$/,
        use: 'url-loader'
      }
    ]
  },
  plugins: [
    new webpack.LoaderOptionsPlugin({
      options: {
        config : {
          path: './postcss.config.js'
        }
      }
    }),
    new ExtracktTextPlugin({
      filename: './app.css'
    })
  ],
  devtool: 'source-map'
};

ところが, v4 からは, extract-text-webpack-plugin を使うとエラーが発生します. 代わりに, mini-css-extract-plugin を使う必要があります. それを使って書き直した webpack.config.js です.

const webpack            = require('webpack');
const ExtracktTextPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: ['./src/main.js', './src/main.css'],
  output: {
    filename: 'app.js',
    path: `${__dirname}`
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          ExtracktTextPlugin.loader,
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.png$/,
        use: 'url-loader'
      }
    ]
  },
  plugins: [
    new webpack.LoaderOptionsPlugin({
      options: {
        config : {
          path: './postcss.config.js'
        }
      }
    }),
    new ExtracktTextPlugin({
      filename: './app.css'
    })
  ],
  devtool: 'source-map'
};

CSS の module.rules.use が変わっていることに着目してください. extract-text-webpack-plugin では extract というクラスメソッドがありましたが, mini-css-extract-plugin では, それがありません. 代わりに, 上記のように loader を指定する必要があります.

コンピュータサイエンスの世界に入って 10 年が経ちました

Prologue

今日, 2018 年 5 月 6 日は, 私がコンピュータサイエンスの世界に入って 10 年が経った日です. つまり, 2008 年 5 月 6 日にコンピュータサイエンスの世界に入りました. きっかけは … 大学時代, とりあえず建築学科に進学しましたが, やる気なしおちゃん … 3 年を棒に振りました. しかし, 大学 4 回生のときに受講したプログラミングの講義をきっかけにこれなら熱中できるかも … と強引な予測のもと, とりあえず基本情報技術者の資格を取得しよう … それが始まりでした. つまり, 10 年前の今日, 基本情報技術者の 1 回目の講義日だったわけです. 普通の人は, いまどきだと, iOS アプリを作ってみたいとか Web アプリを作ってみたいとかプログラミングがコンピュータサイエンスの入り口という方が多いと思いますが, 私の場合は, きっかけがちょっと変わってますよね.

2008

約 3 ヶ月の学習の末, 基本情報技術者を取得することができました. 就職活動もやめていたし, 大学生活が不完全燃焼ということもあって, 専攻を変えて大学院に進学することを決めました. 院試のために, C 言語の基礎中の基礎みたいなのを独学で学びました. 朝は, 大学の図書館で院試の勉強, 昼から夕方まで卒論, 夕方から22 : 00 まで再度, 大学の図書館で院試の勉強という生活が半年ほど続きました.

2009

何はともあれ, 北陸先端科学技術大学院大学に進学したわけですが, 初めて実際にプログラムを書いたり, UNIX OS (Solaris) に触れたり, LaTex に苦戦したりの前半半年を過ごしました. 進学当初は, 音楽が好きだったこともあり, 楽器メーカーで電子楽器の製作 (組み込みプログラミング) をしたいと考えていましたので, 音情報処理研究室を選択しました. しかし, 結構な誤算で音の物理特性や音信号処理の数学には強くなったものの, あまりプログラミングをしたり, コンピュータサイエンスを学んだりするような専攻ではなかったので徐々にモチベーションは下がっていきました.

2010

研究のモチベーションがあがらない, 就職活動でなんども北陸 <-> 東京の往復, 強制収容所での生活などのストレスが重なり, うつになりました.

2011 – 2014

My Library を参照してください.

2015 – 2016

D 社に新卒入社. しかし, まともにプログラミングさせてもらえず, テスター・社内ニートになり, ここでいたら自分の目的, すなわち, 東京のレベルの高い Web 企業でスキルを向上させることが達成できないと考え, 2016 年 10 月 31 日で退社しました. そして, 2016 年 11 月 1 日に C 社に転職しました.

2017 – 2018

たまたま受検した, 応用情報技術者試験をきっかけにコンピュータサイエンスを学ぶ楽しさやスキル向上のために東京にきたことを思い出し, できるだけ仕事を早く終わって, 東京都立図書館にいって学ぶ毎日 … とても充実しています !

Epilogue

この 10 年間を振り返ってみると, いくつかのターニングポイントがあったように思います.

  • 基本情報技術者試験の学習を始めたこと
  • 大学院に進学したこと
  • うつになったこと
  • Y 社から内定をいただき, 入社辞退したこと
  • JavaScript, Web Audio API に出会ったこと
  • 学生 1 年 + ニート 1.5 年 + アルバイト 1.5 年 の 4年間
  • 東京にきたこと
  • 応用情報技術者試験をきっかけに, 忘れかけていたことを思い出したこと

次は, どんなターニングポイントが待っているのかわかりませんが, いつどこでどんなターニングポイントが待っていようと, 自分のやりたいことをやるだけです. 通勤中に自分のプロダクトを実装したり, 技術書を読んだり, 仕事で学んで, 早く帰って図書館にいって学ぶ … もしかすると, そういったルーティンが自分にとってよきターニングポイントを呼び寄せるのかもしれません.