なになれ

IT系のことを記録していきます。

Prettierを導入する際に調べたことまとめ

Prettierとは

コードフォーマッターです。Webフロントエンド向けに対応言語が豊富です。
Webフロントエンドの開発環境ではデファクトスタンダードになっているツールのようです。
GitHubのスター数は29,000を超えています。※2018年12月9日現在

github.com

今回は以前に作成したTypeScriptのプロジェクトにPrettierを導入しました。

hi1280.hatenablog.com

その際に調べたことをまとめます。

導入方法

既にTSLintが導入済みである前提としています。

インストール

必要なnpmモジュールをインストールします。

npm install --save-dev prettier tslint-config-prettier

tslint-config-prettierはTSLintとPrettierとの設定上の競合を防止するために必要なモジュールです。

設定

tslint-config-prettierをTSLintに設定します。

tslint.json(一部抜粋)

    "extends": [
        "tslint:recommended",
        "tslint-config-prettier"
    ],

Prettierの設定ファイルを作成します。

.prettierrc

{
    "printWidth": 120,
    "trailingComma": "all",
    "singleQuote": true
}

好みに合わせてフォーマットのオプションを設定します。
設定の詳細はこちらです。
Options · Prettier

実行確認

これでPrettierが使えます。

下記のようにnpm-scriptsを設定すると実行できます。
package.json(一部抜粋)

"scripts": {
  "format": "prettier --write src/**/*.ts"
}
npm run format

Gitのコミット時に実行する

チームで開発する場合には他のメンバーにも同じコードフォーマットを適用したいところです。
そのために、Gitのコミット時にPretttierを実行します。

インストール

必要なnpmモジュールをインストールします。

npm install --save-dev lint-staged husky

設定

package.jsonを修正します。
npm run lintでlintも合わせて実施するようにしています。

package.json(一部抜粋)

  "scripts": {
    "lint": "tslint -p tsconfig.json --fix"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.ts": ["prettier --write", "npm run lint", "git add"]
  },

参考情報

Pre-commit Hook · Prettier

補足情報

Prettier適用の試行錯誤中に見つけた情報です。

VSCode拡張機能による導入方法

今回はGitのコミットの際にコードフォーマットを実行するようにしました。
その他にもファイルの保存時に実行するようにしたい場合は以下のVSCode拡張機能を試すのが良さそうです。
個人での開発の場合ではこの方法でも良さそうです。

VSCodeで自動整形ツールのprettierで自動フォーマットされるようにする - Qiita

TSLintにPrettierの設定を統合する方法

今回はPrettierの設定とTSLintの設定を別にしました。
tslint-plugin-prettierでTSLintにPrettierを統合できます。
ただ、別々にしても使用感は特に変わりがないと思ったので、この方法は採用しませんでした。

TypeScript + TSLint + Prettier + Webpack環境を作成したときのメモ - Qiita

この方法を試す中で、ハマったポイントがあります。
tslint.jsonに以下のように直接Prettierの設定を書いた場合には、VSCodeのtslintプラグインがPrettierでのルールをtslint上の問題として正常に出力してくれました。

tslint.json

{
  "rulesDirectory": [
    "tslint-plugin-prettier"
  ],
  "extends": [
    "tslint-config-standard",
    "tslint-config-prettier"
  ],
  "rules": {
    "prettier": [
      true,
      {
        "singleQuote": true
      }
    ]
  }
}

Prettierの設定に.prettierrcなどの別ファイルを使用した場合、以下のようにしないと誤った出力になります。
editorconfigを無効化することで、外部ファイルの設定が有効になるようです。

tslint.json

{
  "rulesDirectory": [
    "tslint-plugin-prettier"
  ],
  "extends": [
    "tslint-config-standard",
    "tslint-config-prettier"
  ],
  "rules": {
    "prettier": [true, null, { "editorconfig": false }]
  }
}

参考

Prettier 入門 ~ESLintとの違いを理解して併用する~ - Qiita

Step by step: Building and publishing an NPM Typescript package.

TypeScriptでユニットテストを書く

TypeScriptで作ってみるシリーズの作業ログです。
今回はユニットテストを書きます。

前回はこちらです。

hi1280.hatenablog.com

使用モジュール

  • sinon
  • @types/sinon

https://sinonjs.org/

テストダブルを用意するためのツールと型定義です。

インストール方法

npm install --save sinon
npm install --save @types/sinon

コード

tsconfig.json ※一部抜粋

    "target": "es2015",                       /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */

targetes5だとPromiseの箇所でエラーになってしまったので、es2015に変更しました。

main.spec.ts

import assert = require("assert");
import axios from "axios";
import sinon from "sinon";
import { main } from "../src/main";

describe("main()", () => {
    it("assert response", async () => {
        const resolved = Promise.resolve<any>({
            data: new Array(20),
          });
        sinon.stub(axios, "get").returns(resolved);
        const res = await main();
        assert(res.data.length === 20);
    });
});

axiosによるHTTPレスポンスの部分をスタブに置き換えて、HTTP通信による不安定なテストからスタブによる安定したテストに変えています。

結果

f:id:hi1280:20181205230249g:plain

TypeScriptでWebAPIを実行する

TypeScriptの開発環境ができたので、とりあえずWebAPIを実行するプログラムを作ってみました。

hi1280.hatenablog.com

今回はQiitaのAPIを使いました。

https://qiita.com/api/v2/docs

使用モジュール

コード

main.ts

#!/usr/bin/env node
import axios from "axios";
import { Qiita } from "./qiita";

export async function main() {
  const res = await axios.get<Qiita[]>("https://qiita.com/api/v2/items");
  console.log(res.data);
  return res;
}

if (require.main === module) {
  main();
}

qiita.ts

// Generated by https://quicktype.io

export interface Qiita {
  rendered_body: string;
  body: string;
  coediting: boolean;
  comments_count: number;
  created_at: string;
  group: null;
  id: string;
  likes_count: number;
  private: boolean;
  reactions_count: number;
  tags: Tag[];
  title: string;
  updated_at: string;
  url: string;
  user: User;
  page_views_count: null;
}

export interface Tag {
  name: string;
  versions: any[];
}

export interface User {
  description: null | string;
  facebook_id: null | string;
  followees_count: number;
  followers_count: number;
  github_login_name: null | string;
  id: string;
  items_count: number;
  linkedin_id: null | string;
  location: null | string;
  name: string;
  organization: string | null;
  permanent_id: number;
  profile_image_url: string;
  team_only: boolean;
  twitter_screen_name: null | string;
  website_url: null | string;
}

QiitaAPIからのレスポンスデータの型はJSONからPaste JSON as CodeというVSCode拡張機能で生成しました。
JSONをコピーしてコマンドを実行するだけなので楽チンです。
Paste JSON as Code - Visual Studio Marketplace

ビルド&コマンド実行設定

TypeScriptのコードをJavaScriptにトランスパイルする

npm run build

生成したJavaScriptファイルに実行権限を与える

chmod 755 dist/*

コマンド名と実行ファイルを紐付ける

package.json ※一部省略

"bin": {
  "qiita-cli": "dist/main.js"
},

ローカルの環境でnpm packageのinstallをシミュレートする

npm link

結果

f:id:hi1280:20181205001318g:plain

参考

https://x-team.com/blog/a-guide-to-creating-a-nodejs-command/

VSCodeでTypeScriptのCLIツール作成環境を作る

VSCodeでTypeScriptのCLIツール作成環境を作りました。
npmモジュールを作るのにも応用できると思います。

github.com

以下のような考えで作りました。

  • ローカル環境での実行・デバッグ単体テスト実行はts-nodeで行う
  • VSCodeのタスク機能は使わない想定
  • コードフォーマットにPrettierを使用
  • Gitにコミットする際にコードフォーマットとLintを実行

参考

環境作成について

https://qiita.com/kurogelee/items/cf7954f6c23294600ef2

https://x-team.com/blog/a-guide-to-creating-a-nodejs-command/

https://code.visualstudio.com/docs/editor/variables-reference

https://github.com/TypeStrong/ts-node

Prettierについて

https://qiita.com/soarflat/items/06377f3b96964964a65d

NgRxを試した所感

NgRxはAngularにおける状態管理のためのライブラリです。
ng-conf 2018でも状態管理関連のセッションはNgRxを前提とした内容が多く、デファクトスタンダードなのではと思います。

今回は、Tour of Heroesに対してNgRxを適用してみました。

こちらがコードです。
github.com

今更ながら、Reduxライクな状態管理について勉強しているので、その一貫としてAngularにReduxを適用したいというのが動機です。
NgRxの使い方については末尾の参考を見てもらうとして、以下、使用してみた所感です。

良い面

状態管理に対して厳格な設計を適用できる

NgRxを使うことでActions、ReducersといったReduxの要素をどのように実装すれば良いか明確な指針が示されます。作る側はその通りに作れば良いので、設計を考える必要が無くなります。

Reduxを適用するにあたり考慮しなければいけないこととして、HTTP通信などの外部システムとのやりとりをどうするかといった問題があります。NgRxではこの問題をEffectsで対処しています。

Effectsを使うことで、ActionsからReducersへの処理の流れをそのままにして、HTTPリクエストの処理とHTTPレスポンスの処理とを下記のようなコードで制御することができます。

hero.effects.ts

  @Effect()
  loadHeroes$ = this.actions$.pipe(
    ofType(
      HeroActionTypes.HeroesRequestedDashboard
      ,HeroActionTypes.HeroesRequestedHeroes),
    mergeMap(() => this.heroService.getHeroes()),
    map(heroes => new HeroesLoaded({heroes}))
  );

状態の可視化が楽になる

Redux DevToolsを使い、Action毎に状態を可視化できるのが便利です。
f:id:hi1280:20181201154548p:plain

悪い面

RxJS+Redux+αといった作りが重い

Reduxに対して色々な要素が盛り込まれているので理解して、使いこなすまでに学習コストがかかるライブラリという印象です。
個人的には特にEffectsやSelectors、EntityといったNgRx独自の要素に戸惑いました。

コードの見通しに注意を払う必要がある

状態管理のために必要な要素が多いので、コード量が多くなり、見通しが悪くなります。
モジュールを分けるなどして見通しを良くする工夫を意識する必要がありそうです。

今回試しに作成したReducersはコードの見通しが悪いと感じています。
下記コードの他にEffectsも関係しているので複雑です。

index.ts

import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { ActionReducerMap, createSelector, MetaReducer } from '@ngrx/store';
import { storeFreeze } from 'ngrx-store-freeze';
import { environment } from '../../environments/environment';
import { Hero } from '../hero';
import { HeroActions, HeroActionTypes } from '../hero.actions';

export const adapter: EntityAdapter<Hero> = createEntityAdapter<Hero>();

export interface HeroState extends EntityState<Hero> {}
export interface SearchedHeroState extends EntityState<Hero> {}

export interface AppState {
  hero: HeroState,
  searchedHero: SearchedHeroState
}

export const reducers: ActionReducerMap<AppState> = {
  hero: heroReducer,
  searchedHero: searchedHeroReducer
};

export const initialHeroState: HeroState = adapter.getInitialState();
export const initialSearchedHeroState: SearchedHeroState = adapter.getInitialState();

export function heroReducer(state = initialHeroState, action: HeroActions): HeroState {
  switch (action.type) {
    case HeroActionTypes.HeroesLoaded:
      return adapter.addAll(action.payload.heroes, {...state});
    case HeroActionTypes.HeroLoaded:
    case HeroActionTypes.HeroAddedSuccess:
      return adapter.addOne(action.payload.hero, state);
    case HeroActionTypes.HeroUpdated:
      return adapter.updateOne(action.payload.hero, state);
    case HeroActionTypes.HeroDeleted:
      return adapter.removeOne(action.payload.hero.id, state);
    default:
      return state;
  }
}

export function searchedHeroReducer(state = initialSearchedHeroState, action: HeroActions): SearchedHeroState {
  switch (action.type) {
    case HeroActionTypes.HeroesSearchLoaded:
      return adapter.addAll(action.payload.heroes, {...state});
    default:
      return state;
  }
}

export const selectHeroState = (state: AppState) => state.hero;
export const selectHeroes = createSelector(selectHeroState, adapter.getSelectors().selectAll);
export const selectHeroesById = (id: number) => createSelector(
  selectHeroState,
  (state: HeroState)  => state.entities[id]);
export const selectSearchedHeroState = (state: AppState) => state.searchedHero;
export const selectSearchedHeroes = createSelector(selectSearchedHeroState, adapter.getSelectors().selectAll);

export const metaReducers: MetaReducer<AppState>[] = !environment.production ? [storeFreeze] : [];

注意点

Actionsの定義

NgRxだと、ユーザの画面操作、Web APIからのレスポンスといったActionを細かに定義することが求められます。これはRedux DevToolsを使った状態の変化を追跡するのに役に立ちます。

しかし、Actionを細かに定義するのは面倒と感じることもあるように思います。単純に数が増えるのでActionをどのように定義すれば良いか迷います。

Actionにおけるtypeの一例が下記です。

hero.actions.ts

export enum HeroActionTypes {
  HeroesRequestedDashboard = '[Dashboard Page] Heroes Requested',
  HeroesRequestedHeroes = '[Heroes Page] Heroes Requested',
  HeroesLoaded = '[Heroes API] Heroes Loaded',
  HeroRequested = '[Hero Page] Hero Requested',
  HeroLoaded = '[Heroes API] Hero Loaded',
  HeroAdded = '[Heroes Page] Hero Added',
  HeroUpdated = '[Hero Detail Page] Hero Updated',
  HeroDeleted = '[Heroes Page] Hero Deleted',
  HeroAddedSuccess= '[Heroes API] Hero Added Success',
  HeroesSearchRequested = '[Hero Search Page] Heroes Search Requested',
  HeroesSearchLoaded = '[Heroes API] Heroes Search Loaded'
}

イベントの発生源とイベントの内容を明確に定義することが推奨されています。

まとめ

NgRxは大規模で複雑な状態管理を求められるアプリに対して適用するというのが分かりやすい判断基準だと思いますが、コストに見合った恩恵を得られるのか十分に検討する必要がありそうです。

今回の取り組みで既存のServiceを使いつつ、NgRxが適用可能なことを確認できました。
AngularはServiceを使うことである程度の状態管理ができます。まずはServiceで状態管理を始めて、状態管理が複雑になってきたらNgRxを適用するのが開発効率を考えると筋が良いように思います。

参考

https://ngrx.io/

https://qiita.com/Yamamoto0525/items/03155acbce1ffd1d037e

https://www.udemy.com/share/100cf4AkAfeV1QQHQ=/

Tour of HeroesをVue+Vuexで再実装した

Vue+Vuexを勉強したアウトプットとしてアプリを作りました。
hi1280.hatenablog.com

作ったもの

Tour of HeroesはAngular公式のチュートリアルです。
複数画面があって、WebAPIを呼び出すサンプルになっています。
今回はこのTour of Heroesを題材にして、Vue+Vuexを使ってTour of Heroesを再実装しました。

実際のプログラムはこちらです。
github.com

Angular、React+Reduxと比較しつつ、Vue+Vuexによる実装内容を説明します。

Viewの作成

リスト形式でデータを表示する

VueではHTMLに対してテンプレート構文を書いて動的にHTMLを生成できるようになっています。
v-forというテンプレート構文で繰り返しの記述をサポートしています。
コードを見てもらえば分かるようにAngularにかなり似ています。

Dashboard.vue

<!-- 一部抜粋 -->
<router-link v-for="hero in sliced_heroes" :to="{ name: 'detail', params: {id: hero.id} }" class="col-1-4" :key="hero.id">
  <div class="module hero">
    <h4>{{hero.name}}</h4>
  </div>
</router-link>

ちなみにAngularの場合はngForというテンプレート構文になります。

dashboard.component.html

<!-- 一部抜粋 -->
<a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}">
  <div class="module hero">
    <h4>{{hero.name}}</h4>
  </div>
</a>

フォームを作成する

v-modelというテンプレート構文で双方向データバインディングが可能です。
バインディングするデータの元はVuexで定義したStoreです。

HeroDetail.vue

// 一部抜粋
<template>
  <div v-if="hero">
    <h2>{{hero.name}} Details</h2>
    <div><span>id: </span>{{hero.id}}</div>
    <div>
      <label>name:
        <input v-model="hero.name" placeholder="name"/>
      </label>
    </div>
    <button @click="goBack()">go back</button>
    <button @click="save()">save</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      hero: {
        id: 0,
        name: ''
      }
    }
  },
  created() {
    this.$store.dispatch('fetchHero', this.$route.params.id);
    this.hero = this.$store.getters.heroes.find(element => element.id == this.$route.params.id);
  },
  methods: {
    goBack() {
      this.$router.go(-1);
    },
    save() {
      this.$store.dispatch('updateHero', this.hero);
      this.$router.go(-1);
    }
  }
}
</script>

Angularのチュートリアルでも双方向データバインディングでフィールドの値とフォームの値とを同期しています。

hero-detail.component.html

<!-- 一部抜粋 -->
<input [(ngModel)]="hero.name" placeholder="name"/>

hero-detail.component.ts

// 一部抜粋
export class HeroDetailComponent implements OnInit {
  @Input() hero: Hero;
}

WebAPI呼び出し

VuexのActionでWebAPIを呼び出します。
HTTPのリクエストを実行するのにvue-resourceを使いました。
なお、vue-resourceは公式で推奨するやり方ではなくなっているようです。

modules/heroes.js

// 一部抜粋
const actions = {
  fetchHeroes: ({commit}) => {
    Vue.http.get(`?key=${key}`)
      .then(response => response.json())
      .then(heroes => {
        if(heroes) {
          commit('FETCH_HEROES', heroes);
        }
      });
  },

Actionでの非同期処理の結果をMutationで処理します。

modules/heroes.js

// 一部抜粋
const mutations = {
  'FETCH_HEROES' (state, heroes) {
    state.heroes = heroes;
  },

ちなみにReduxのコードです。
ActionではVuexとは違い、非同期処理によるPromiseオブジェクトをそのまま渡しています。

actions/index.js

// 一部抜粋
export function fetchHeroes() {
  const request = axios.get(`${ROOT_URL}?key=${key}`);
  return {
    type: FETCH_HEROES,
    payload: request
  }
}

ActionからReducerに処理が流れる時に、redux-promiseを使って、PromiseのオブジェクトをJavaScriptのオブジェクトに変換しています。
ReducerではPromiseの値であることを意識せずに処理します。

reducer-heroes.js

// 一部抜粋
export default function(state = {}, action) {
  switch(action.type) {
    case DELETE_HERO:
      return _.omit(state, action.payload);
    case FETCH_HEROES:
      return _.mapKeys(action.payload.data, 'id');
    case CREATE_HERO:
    case FETCH_HERO:
    case UPDATE_HERO:
      return {...state, [action.payload.data.id]: action.payload.data};
    default:
      return state;
  }
}

CSS指定

グローバルなCSSやScopedなCSSを記述することが可能です。
以下のようにしてコンポーネント内で閉じたCSS指定が可能です。

App.vue

/* 一部抜粋 */
<style scoped>
h1 {
  font-size: 1.2em;
  color: #999;
  margin-bottom: 0;
}
h2 {
  font-size: 2em;
  margin-top: 0;
  padding-top: 0;
}
nav a {
  padding: 5px 10px;
  text-decoration: none;
  margin-top: 10px;
  display: inline-block;
  background-color: #eee;
  border-radius: 4px;
}
nav a:visited, a:link {
  color: #607D8B;
}
nav a:hover {
  color: #039be5;
  background-color: #CFD8DC;
}
nav a.active {
  color: #039be5;
}
</style>

Angularでも同様にコンポーネント内に閉じたCSS指定が可能です。

まとめ

今回取り組んでみて、Vue+VuexではAngularやReduxの良いところを取り入れている感じがしました。
ActionやMutationの部分など、VuexはReduxに比べて作りがシンプルな感じがしました。

UdemyのVue.jsコース「Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)」を受講した感想

www.udemy.com

こちらを利用して、Vue.jsを基礎から学びました。
例によって、レビューの評価が高いので、このコースを選びました。コースの内容を見てもかなり網羅されている感じで良いと思います。
このコースはレクチャーの数がかなり多かったです。

良い点

基礎的な内容を細かく説明している

Vue.jsの機能をコードを通じて、事細かに説明しています。
vue-routerの解説はレクチャー数も多く、充実しているのではないかと思います。
Formの解説でもテキストボックスやチェックボックスといった各要素をVue.jsでどのように扱うかといった細かいレクチャー分けがされています。

サンプルを作るレクチャーがある

コースの内容としてはVue.jsの機能を説明するレクチャーがほとんどです。
ただ合間にこれまで説明した機能を使ったサンプルを作るレクチャーがあるので、機能をどう使うかといった勘所も得られる内容になっていると思います。

悪い点

レクチャーの数がかなり多い

Complete Guideというだけあって、受講必須なレクチャーだけを受講したとしても時間が長いです。全体の動画時間は22時間ほどです。
また、サンプルを作りながら学ぶという形式ではなく、Vue.jsの機能を紹介するためのレクチャーが続くのでモチベーションを持ち続けるのが難しいかもしれません。

まとめ

今回は1400円で購入しました。
Vue.jsは1つのJavaScriptファイルを読み込むだけで使い始めることができるのが簡単で良いと思いました。
今回のコース受講を通してVue.jsは初学者向きという印象を持ちました。
Reactは先進的、Angularは安心というのが個人的な所感です。