Nuxt.js 学習メモ(公式ドキュメントの雑な抜粋)

これは何か

Nuxt.js の公式ドキュメントを読んだときのメモです。
気になったところ、後でもう一度確認が必要そうなもの、など。
Nuxt.js のバージョンは v1.4.2。

Nuxt.js ガイド

プロローグ

f:id:ryotah:20180825174314p:plain

  • サーバーサイドレンダリング
    • nuxt コマンドを実行すると開発サーバーが起動されます。このサーバーはホットリローディング及び vue-server-render を備えており、vue-server-render は自動的にアプリケーションをサーバーサイドレンダリングするよう設定されています。

  • nuxt --spa
  • 静的ファイルの生成
    • (必要になったら読む)

はじめる

インストール

ディレクトリ構造

設定

  • nuxt.config.js
  • (必要になったら読む)

ルーティング

  • Nuxt.js は pages ディレクトリ内の Vue ファイルの木構造に沿って、自動的に vue-router の設定を生成します。

  • 動的なルーティング
    • users/_id.vue => /users/:id?, _slug => path: '/:slug',
    • user-id と名付けられたルートに :id? というパスがありますが、これはこの :id が必須ではないことを表します。もし必須にしたい場合は users/_id ディレクトリ内に index.vue ファイルを作成してください。

    • ルーティングのパラメータのバリデーション
<script>
  export default {
    validate ({ params }) {
      // 数値でなければならない
      return /^\d+$/.test(params.id)
    }
  }
</script>

ビュー

  • ドキュメント
    • HTML テンプレートを拡張するために、プロジェクトのルートディレクトリに app.html を作成します。

  • レイアウト
    • デフォルトレイアウト
      • layouts/default.vue ファイルを追加することでメインレイアウトを拡張できます。

      • 必ず コンポーネントを入れておくことを覚えておいてください。

    • エラーページ
      • layouts/error.vue ファイルを追加することでエラーページをカスタマイズできます。

      • <nuxt/> を含めてはならない

    • カスタムレイアウト
      • layouts ディレクトリの 第一階層 のファイルで、ページコンポーネント内の layout プロパティで指定できるカスタムレイアウトを作成できます。

      • pages/posts.vue ファイル内で、カスタムレイアウトを使うことを Nuxt.js に伝えます:

  • ページ
    • asyncData, fetch, head, layout, transition, scrollToTop, validate, middleware
  • HTML ヘッド
    • Nuxt.js は headers とアプリケーションの html attributes を更新するために vue-meta を使用しています。Nuxt.js はこれらのオプションで vue-meta を設定します:

    • Nuxt.jsでは、nuxt.config.js 内にデフォルトの タグを全て定義することができます。

非同期なデータ

  • Nuxt.js はコンポーネントのデータをセットする前に非同期の処理を行えるようにするために asyncData メソッドを追加しています。

  • asyncData メソッド
    • promise, async/await, callback のどれか
    • asyncData の結果はコンポーネントのデータと マージされ ます

  • コンテキスト
    • context 内で利用できるキーの一覧
    • 動的なルートデータへのアクセス
      • たとえば、動的ルートパラメータには、コンテキストオブジェクトを構成したファイルまたはフォルダの名前を使用してアクセスできます。 したがって、_slug.vue という名前のファイルを定義する場合、context.params.slug を介してアクセスできます。

    • クエリの変化のリスニング
      • デフォルトでは、クエリストリングの変化で asyncData メソッドは呼ばれません。ページネーションコンポーネントのビルド時などにこの振る舞いを変更したい場合は、ページコンポーネントの watchQuery プロパティを見るパラメータを設定することができます。

      • watchQuery プロパティ
  • エラー処理
    • Nuxt.js は、context に error (パラメータ)メソッドを追加し、エラーページを表示するためにそれを呼び出すことができます。

    • params.statusCode は、サーバーサイドから適切なステータスコードを表示するためにも使用されます。

    • callback({ statusCode: 404, message: 'ページが見つかりません' })
      • 第一引数を利用する

アセット

  • Webpack で取り扱う
    • デフォルトでは vue-loader は css-loader および vue-template-compiler を用いて、スタイルやテンプレートファイルを処理します。このコンパイル処理の中で、<img src="...">background: url(...)CSS @import などのすべての URL はモジュールの依存関係のように解決されます。

  • Webpack で扱わない静的ファイル

プラグイン

  • Nuxt.js では js プラグインを定義することができ、それはルートの Vue.js アプリケーションがインスタンス化される前に実行されます。

  • 外部パッケージの利用
    • こうすれば、バンドルファイルが膨れ上がることなく、どの場所にも import axios と書くことができます。

module.exports = {
  build: {
    vendor: ['axios']
  }
}

モジュール

  • モジュールは、Nuxt 起動時に順番に呼び出される、シンプルな関数です

  • Webpack の Tapable に基づいた Nuxt のモジュール設計のおかげで、モジュールは例えばビルドの初期化のような特定のエントリーポイントに、フックを簡単に登録できるのです。

  • 基本的なモジュールを書く
  • 非同期モジュール
  • 共通のスニペット
    • (後で読む)
  • 特定のフックでタスクを実行する
    • 単に Nuxt の初期化処理時だけではなく、特定の条件下でのみ、モジュールにある処理を実行させたいこともあるでしょう。

Vuex ストア

  • Nuxt.js は store ディレクトリを探索し存在するときには以下を実行します: 1. Vuex をインポートします 2. vuex モジュールを vendor のバンドルファイルに追加します 3. store オプションをルートの Vue インスタンスに追加し

  • クラシックモード
    • ストアをクラシックモードで有効にするには store/index.js ファイルを作成し、ストアインスタンスをエクスポートします:

  • モジュールモード
    • このオプションを使いたいときは、ストアインスタンスの代わりに、store/index.js 内でステート、ミューテーション、アクションをエクスポートします:

  • fetch メソッドは、ページがレンダリングされる前に、データをストアに入れるために使われます。

  • nuxtServerInit アクション
    • nuxtServerInit というアクションがストア内に定義されているときは、Nuxt.js はそれをコンテキストとともに呼び出します(ただしサーバーサイドに限ります)。サーバーサイドからクライアントサイドに直接渡したいデータがあるときに便利です。

    • コンテキストは、asyncDataや fetch メソッドと同様に nuxtServerInit に第二引数として渡されます。

  • Vuex Strict モード
    • Strict モードは dev モードではデフォルトで有効化されており、production モードでは無効化されています。strict モードを dev で無効化するには、以下の例を参照してください。

コマンド

  • コマンド一覧
    • nuxt
      • 開発サーバー起動
    • nuxt build
      • Webpack でビルド、ミニファイ
    • nuxt start
      • プロダクションモードでサーバーを起動(nuxt build 後に実行)
    • nuxt generate
  • プロダクションのデプロイ
    • Nuxt.js では 3つのモードからアプリケーションのデプロイを選択できます。サーバレンダリング、SPA、そして静的生成です。

    • (必要になったら読む)

開発ツール

  • エンドツーエンドテスト
    • (必要になったら読む)
  • ESLint と Prettier
    • (必要になったら読む)

その他

Vuex 学習メモ

これは何か

Vuex の公式ドキュメントを読んだときのメモです。
気になったところ、後でもう一度確認が必要そうなもの、など。
Vuex のバージョンは v3.0.1。

Vuex ガイド

Vuex とは何か?

  • 他のパターンと異なるのは、Vuex は効率的な更新のために、Vue.js の粒度の細かいリアクティビティシステムを利用するよう特別に調整して実装されたライブラリだということです。

f:id:ryotah:20180824202715p:plain

Vuex 入門

    1. Vuex ストアはリアクティブです。Vue コンポーネントがストアから状態を取り出すとき、もしストアの状態が変化したら、ストアはリアクティブかつ効率的に更新を行います。
    2. ストアの状態を直接変更することはできません。明示的にミューテーションをコミットすることによってのみ、ストアの状態を変更します。これによって、全ての状態の変更について追跡可能な記録を残すことが保証され、ツールでのアプリケーションの動作の理解を助けます。
  • store.state.count を直接変更する代わりにミューテーションをコミットする理由は、状態の変更を明確に追跡したいからです。このシンプルな規約は、あなたのコードの意図をさらに明確にし、コードを読んだ時にアプリケーションの状態の変更について、論理的に考えることができるようにします。加えて、私たちに全ての変更のログを取ったり、状態のスナップショットを取ったり、タイムトラベルデバッグを行うようなツールを実装する余地を与えてくれます。

コアコンセプト

ステート

  • Vuex は 単一ステートツリー (single state tree) を使います。

  • mapState ヘルパー

ゲッター

  • 算出プロパティと同様に、ゲッターの結果はその依存関係に基づいて計算され、依存関係の一部が変更されたときにのみ再評価されます。

  • プロパティスタイルアクセス
    • ゲッターは store.getters オブジェクトから取り出され、プロパティとしてアクセスすることができます:

    • ゲッターは第2引数として他のゲッターを受け取ります:

    • プロパティとしてアクセスされるゲッターは Vue のリアクティブシステムの一部としてキャッシュされるという点に留意してください。

  • メソッドスタイルアクセス
    • 関数を返り値にすることで、ゲッターに引数を渡すこともできます。

    • メソッドによってアクセスされるゲッターは呼び出す度に実行され、その結果はキャッシュされない点に留意してください。

  • mapGetters ヘルパー

ミューテーション

アクション

  • アクションは、状態を変更するのではなく、ミューテーションをコミットします。 アクションは任意の非同期処理を含むことができます。

  • したがって context.commit を呼び出すことでミューテーションをコミットできます。あるいは context.statecontext.getters で、状態やゲッターにアクセスできます。

  • なぜコンテキストオブジェクトがストアインスタンスそのものではないのかは、後ほどモジュールで説明します。

  • mapActions ヘルパー
  • vuex-101-promise - StackBlitz
    • Promise, async / await を利用したデモ
実践的な例として、ショッピングカートをチェックアウトするアクション
actions: {
  checkout ({ commit, state }, products) {
    // 現在のカート内の商品を保存する
    const savedCartItems = [...state.cart.added]
    // チェックアウトのリクエストを送信し、楽観的にカート内をクリアする
    commit(types.CHECKOUT_REQUEST)
    // shop API は成功時のコールバックと失敗時のコールバックを受け取る
    shop.buyProducts(
      products,
      // 成功時の処理
      () => commit(types.CHECKOUT_SUCCESS),
      // 失敗時の処理
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

モジュール

  • vuex-101-module - StackBlitz
    • デモ
  • context.state, context.rootState
  • 名前空間
    • デフォルトでは、モジュール内部のアクション、ミューテーション、そしてゲッターはグローバル名前空間の元で登録されます - これにより、複数のモジュールが同じミューテーション/アクションタイプに反応することができます。

    • namespaced: true
    • 名前空間付きモジュールでのグローバルアセットへのアクセス
      • アクションをディスパッチするか、グローバル名前空間にミューテーションをコミットするには、dispatch と commit の3番目の引数として {root: true} を渡します。

    • 名前空間によるバインディングヘルパー
      • ...mapState({ a: state => state.some.nested.module.a,
      • ...mapState('some/nested/module', {,
      • const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
  • 動的にモジュールを登録する
    • store.registerModule
    • (あとで読む)
  • モジュールの再利用
    • (あとで読む)

アプリケーションの構造

プラグイン

厳格モード

  • strict: true
  • 本番環境で厳格モードを有効にしてデプロイしてはいけません! 厳格モードでは不適切なミューテーションを検出するためにステートツリーに対して深い監視を実行します。パフォーマンスコストを回避するために本番環境では無効にしてください。

フォームの扱い

  • vuex-101-forms - StackBlitz
    • デモ
  • 厳格モードでは、この変更は明示的に Vuex のミューテーションハンドラ内部で処理されていないため、エラーを投げます。

テスト

  • (あとで読む)

ホットリローディング

  • (あとで読む)

Vue.js 学習メモ

これは何か

Vue.js の公式ドキュメントを読んだときのメモです。
気になったところ、後でもう一度確認が必要そうなもの、など。
Vue.js のバージョンは v2.5.17。

Vue.js ガイド

基本的な使い方

f:id:ryotah:20180825165147p:plain

  • テンプレート構文
    • v-once, v-html, v-bind など
    • {{ number + 1 }}
      • 単一の式だけ含むことができるというものです

      • テンプレート式はサンドボックスで、Math や Date といったホワイトリストにあるグローバルオブジェクトだけアクセスできます。テンプレート式内でユーザーが定義したグローバルオブジェクトにアクセスしようとしてはいけません。

    • ディレクティブ
      • ディレクティブは v- から始まる特別な属性です

      • 修飾子
        • .prevent 修飾子
          • v-on:submit.prevent="onSubmit"
    • 省略記法
      • v-bind => :
      • v-on => @
  • 算出プロパティとウォッチャ
    • 算出プロパティ
      • computed
        • Vue は vm.reversedMessage が vm.message に依存していることを知っているので、vm.message が変わると vm.reversedMessage に依存する全てのバインディングを更新します。

      • 算出プロパティ vs メソッド
        • 算出プロパティは依存関係にもとづきキャッシュされるという違いがあります。

      • 算出プロパティ vs 監視プロパティ
        • 汎用的なものもあるよ
      • 算出 Setter 関数
        • setter 関数もある
    • ウォッチャ
      • データが変わるのに応じて非同期やコストの高い処理を実行したいときに最も便利です。

  • クラスとスタイルのバインディング
  • 条件付きレンダリング
    • vue-101-conditional - StackBlitz
      • デモ
    • key による再利用可能な要素の制御
      • しかしこれは必ずしも望ましいことでないかもしれないので、Vue は”この 2 つの要素は完全に別個のもので、再利用しないでください” と伝える方法を提供します。それは、一意の値を持つ key 属性を追加するだけです:

    • v-show は <template> 要素をサポートせず、v-else とも連動しないということに注意してください。

    • ー般的に、v-if はより高い切り替えコストを持っているのに対して、 v-show はより高い初期描画コストを持っています。

  • リストレンダリング
    • vue-101-list - StackBlitz
      • デモ
    • 繰り返される DOM の内容が単純な場合や、性能向上のために標準の動作に意図的に頼る場合を除いて、可能なときはいつでも v-for に key を与えることが推奨されます。

    • 配列の変化を検出
      • 変更メソッド
      • 配列の置き換え
        • これは、Vue が既存の DOM を捨てて、リスト全体を再描画の原因になると思うかもしれません。幸いにもそれはそうではありません。Vue は DOM 要素の再利用を最大化するためにいくつかのスマートなヒューリスティックを実装しているので、重複するオブジェクトを含んでいる他の配列を配列で置き換えることは、とても効率的な作業です。

    • オブジェクトの変更検出の注意
    • フィルタ/ソートされた結果の表示
    • 範囲付き v-for
    • <template> での v-for
    • v-for と v-if
      • それらが同じノードに存在するとき、 v-for は v-if よりも高い優先度を持ちます。これは v-if がループの各繰り返しで実行されることを意味します。

    • コンポーネントと v-for
      • 2.2.0 以降で、コンポーネントで v-for を使用するとき、key は必須です

      • is="todo-item" 属性に注意してください。 これは DOM テンプレートでは必要で、なぜなら <ul> 要素の中では <li> のみが有効だからです。これは <todo-item> と同じ意味ですが、潜在的なブラウザの解析エラーを回避します。詳細は DOM テンプレート解析の注意事項 を参照してください。

  • イベントハンドリング
    • vue-101-events - StackBlitz
      • デモ
    • イベント修飾子
    • キー修飾子
      • グローバルな config.keyCodes オブジェクト経由でカスタムキー修飾子のエイリアスも定義できます

      • KeyboardEvent.keyで公表されている任意の有効なキー名をケバブケースに変換することによって、修飾子として直接使用できます。

    • システム修飾子キー
      • .exact 修飾子
      • マウスボタンの修飾子
    • ViewModel が消去されるときに、すべてのイベントリスナーは自動で削除されます。手動でそれらの消去をおこなうことを気にする必要はありません。

  • フォーム入力バインディング
  • コンポーネントの基本

Components In-Depth

トランジションとアニメーション

  • (必要になったら)

再利用と構成

  • vue-101-reusability-and-composition - CodeSandbox
    • デモ
  • ミックスイン
    • コンポーネントがミックスインを使用するとき、ミックスインの全てのオプションはコンポーネント自身のオプションに”混ぜられ”ます。

    • オプションのマージ
      • 例えば、データオブジェクトはシャローマージ(1層のプロパティ)され、コンフリクトした場合にはコンポーネントのデータが優先されます。

      • 同じ名前のフック関数はそれら全てが呼び出されるよう配列にマージされます。ミックスインのフックはコンポーネント自身のフック前に呼ばれます。

      • などなど
    • グローバルミックスイン
      • 多くのケースでは、上記の例のような、カスタムオプションを処理するようなものに使用すべきです。また、重複適用を避けるため プラグイン としてそれらを作成することをお勧めします。

    • カスタムオプションのマージストラテジ
      • カスタムオプションがマージされるとき、それらは単純に既存の値を上書きするデフォルトのストラテジを使用します。カスタムロジックを使用してカスタムオプションをマージする場合、Vue.config.optionMergeStrategies をアタッチする必要があります:

  • カスタムディレクティブ
  • 描画関数とJSX
    • ノード、ツリー、および仮想 DOM
      • 全ての要素はノードです。全てのテキストはノードです。コメントさえもノードです!

      • 仮想 DOM
        • Vue は、実際の DOM に加える必要がある変更を追跡する仮想 DOM を構築することで、これを達成します。

        • return createElement('h1', this.blogTitle)
    • createElement 引数, 素の JavaScript によるテンプレートの書き換え, JSX, 関数型コンポーネント, テンプレートのコンパイル
      • (後で読む)
  • プラグイン
    • プラグインは通常 Vue にグローバルレベルで機能を追加します。

    • 書くことができるプラグインはいくつかのタイプがあります:

      • グローバル・メソッドを追加
      • グローバル・アセットを追加(ディレクティブ/フィルタ/トランジションなど)
      • グローバル・ミックスインにより、1つ、または複数のコンポーネントオプションを追加
      • Vue.prototype に記述することにより、1つまたは、複数の Vue インスタンスメソッドを追加
    • Vue.use() グローバルメソッドを呼びだすことによってプラグインを使用します:

    • Vue.use は、同じプラグインを 1 回以上使用することを自動的に防ぎます。

  • フィルター

ツール

  • (後でよむ)

スケールアップ

内部

移行

  • (必要になったら)

その他

  • (必要になったら)

その他のドキュメントへのリンク

(WIP) Learn Angular

これは何か

  • Angular を習得するために必要な情報をまとめてみようとしたもの
  • Angular の 公式ドキュメントからいくつか抜粋したもの + α です。Qita や 「Angular デベロッパーズガイド」で代替するのも :ok_hand:
  • (もとは会社用資料です。多少業務内容の影響をうけているので、優先度が少し一般的ではなかったりするかもしれません。)

始める前に

最初に

基礎

実際のコードを書く前に一通り目を通したほうが、つまずきにくくなりそうなドキュメント。

基礎(適宜)

実際のコードを書きながら気になった時に適宜読むといいかも。

応用

RxJS

ngrx

TypeScript

VS Code エクステンション

for Angular

for TypeScript

for Lint and Code format

Util

(WIP) Angular Testing メモ

これは何か

Angular の公式ドキュメント(Angular - Testing)とテストに関する前提知識(用語、Jasmine使い方など)をまとめようとしたもの。

前提知識

Jasmine

spyOn(オブジェクト, 'メソッド名')

// 本来の実装も実行
spyOn(obj, 'method').and.callThrough();

// 戻り値を変更
spyOn(obj, 'method').and.returnValue('stub');

// 処理を任意の関数に置き換える
spyOn(obj, 'method').and.callFake(...)

calls.count();
calls.first();
calls.mostRecent();

// 裸のスパイ関数
var spyFunction = jasmine.createSpy();

// 裸のスパイオブジェクト
var spyObj = jasmine.createSpyObj('spyName', ['hoge', 'fuga', 'piyo']);

用語

  • テストダブル - Wikipedia
  • テストダブル (Test Double) とは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。ダブルは代役、影武者を意味する

    • テストスタブ (テスト対象に「間接的な入力」を提供するために使う。)
    • テストスパイ (テスト対象からの「間接的な出力」を検証するために使う。出力を記録しておくことで、テストコードの実行後に、値を取り出して検証できる。)
    • モックオブジェクト (テスト対象からの「間接的な出力」を検証するために使う。テストコードの実行前に、あらかじめ期待する 結果を設定しておく。検証はオブジェクト内部で行われる。)
    • フェイクオブジェクト (実際のオブジェクトに近い働きをするが、より単純な実装を使う。例として、実際のデータベースを置き換えるインメモリデータベースが挙げられる。)
    • ダミーオブジェクト (テスト対象のメソッドがパラメータを必要としているが、そのパラメータが利用されない場合に渡すオブジェクト。
  • スタブ - Wikipedia

    • スタブ (stub) とは、コンピュータプログラムのモジュールをテストする際、そのモジュールが呼び出す下位モジュールの代わりに用いる代用品のこと。

  • FakeとMockとStubの違い - kkamegawa’s weblog
    • Stub: Prevent the dependency from obstructing the method-under-test and torespond in a way that helps it proceed through its logical steps.

    • Mock: Allow the test code to verify that the code-under-test’s interaction with the dependency is proper, valid, and expected.

Service Tests

https://angular.io/guide/testing#service-tests

コンポーネントのテストは複雑になりがちなので、まずはサービスのテストから。

依存がない場合

service = new ValueService();

他のサービスに依存している場合

// 1. 実際のサービスをInject
masterService = new MasterService(new ValueService());

// 2. FakeサービスをInject
masterService = new MasterService(new FakeValueService());

// 3. Fake objectをInject
const fake =  { getValue: () => 'fake value' };
masterService = new MasterService(fake as ValueService);

// 4. Spy serviceをInject
const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValue']);
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
masterService = new MasterService(valueServiceSpy);

実際のアプリケーションと同じように Angular の DI を利用する必要があるのであれば TestBed を利用。
https://angular.io/guide/testing#angular-testbed

let service: ValueService;

beforeEach(() => {
  TestBed.configureTestingModule({ providers: [ValueService] });
});

it('should use ValueService', () => {
  service = TestBed.get(ValueService);
  expect(service.getValue()).toBe('real value');
});

Testing without beforeEach()

https://angular.io/guide/testing#testing-without-beforeeach

beforeEachを必ずしも利用する必要はない

function setup() {
  const valueServiceSpy =
    jasmine.createSpyObj('ValueService', ['getValue']);
  const stubValue = 'stub value';
  const masterService = new MasterService(valueServiceSpy);

  valueServiceSpy.getValue.and.returnValue(stubValue);
  return { masterService, stubValue, valueServiceSpy };
}

it('#getValue should return stubbed value from a spy', () => {
  const { masterService, stubValue, valueServiceSpy } = setup();
  // ...
});

Testing HTTP services

https://angular.io/guide/testing#testing-http-services

HttpClientのSpyをつくる

beforeEach(() => {
  httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
  TestBed.configureTestingModule({
    providers: [
      ApiService,
      { provide: HttpClient, useValue: httpClientSpy },
    ],
  });
});
// or
beforeEach(() => {
  httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
  service = new ApiService(<any>httpClientSpy);
});
// 正常系
httpClientSpy.get.and.returnValue(asyncData(json));
// エラー系
httpClientSpy.get.and.returnValue(asyncError(errorResponse));

Component Test Basics

https://angular.io/guide/testing#component-test-basics

大抵の場合、DOM関与なしでコンポーネントクラスだけをテストした方が、動作の多くを簡単に検証できる。実際、単純なコンポーネントならnew FooComponent()のようにインスタンスを生成するだけでテストができる。

// component
export class DashboardHeroComponent {
  @Input() hero: Hero;
  @Output() selected = new EventEmitter<Hero>();
  click() { this.selected.emit(this.hero); }
}

// testing
it('raises the selected event when clicked', () => {
  const comp = new DashboardHeroComponent();
  const hero: Hero = { id: 42, name: 'Test' };
  comp.hero = hero;

  comp.selected.subscribe(selectedHero => expect(selectedHero).toBe(hero));
  comp.click();
});

依存関係がある場合はTestBedを利用する。

beforeEach(() => {
  TestBed.configureTestingModule({
    // provide the component-under-test and dependent service
    providers: [
      WelcomeComponent,
      { provide: UserService, useClass: MockUserService }
    ]
  });
  // inject both the component and the dependent service.
  comp = TestBed.get(WelcomeComponent);
  userService = TestBed.get(UserService);
});

// ...

// 手動で ngOnInit 
it('should welcome logged in user after Angular calls ngOnInit', () => {
  comp.ngOnInit();
  expect(comp.welcome).toContain(userService.user.name);
});

Component DOM testing

https://angular.io/guide/testing#component-dom-testing

DOMのテストも必要な場合。コンポーネントを生成してテストする必要がある。

コンポーネントを生成するテストコード

describe('BannerComponent (minimal)', () => {
  it('should create', () => {
    TestBed.configureTestingModule({
      declarations: [ BannerComponent ]
    });
    const fixture = TestBed.createComponent(BannerComponent);
    const component = fixture.componentInstance;
    expect(component).toBeDefined();
  });
});

nativeElement

https://angular.io/guide/testing#nativeelement

DOMのAPIを利用したい場合。

const bannerElement: HTMLElement = fixture.nativeElement;
const p = bannerElement.querySelector('p');

DebugElement

https://angular.io/guide/testing#debugelement

The DebugElement offers query methods that work for all supported platforms.

Component Test Scenarios

https://angular.io/guide/testing#component-test-scenarios

Component binding

https://angular.io/guide/testing#component-binding

Change an input value with dispatchEvent()

input elementの場合はdispatchEventも実行させる。

const nameInput: HTMLInputElement = hostElement.querySelector('input');

// ...

// simulate user entering new name into the input box
nameInput.value = inputName;

// dispatch a DOM event so that Angular learns of input value change.
nameInput.dispatchEvent(newEvent('input'));

// Tell Angular to update the display binding through the title pipe
fixture.detectChanges();

Component with external files

ng testなら不要

Component with a dependency

https://angular.io/guide/testing#component-with-a-dependency

依存関係があるコンポーネント

// providers: [ UserService ] // NO! Don't provide the real service!
// Provide a test-double instead

Provide service test doubles

https://angular.io/guide/testing#provide-service-test-doubles

A component-under-test doesn't have to be injected with real services. In fact, it is usually better if they are test doubles (stubs, fakes, spies, or mocks).

let userServiceStub: Partial<UserService>;

userServiceStub = {
  isLoggedIn: true,
  user: { name: 'Test User'}
};
  • Get injected services
// UserService actually injected into the component
userService = fixture.debugElement.injector.get(UserService);

// UserService from the root injector
userService = TestBed.get(UserService);

Component with async service

https://angular.io/guide/testing#component-with-async-service

Testing with a spy

const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
// Make the spy return a synchronous Observable with the test data
getQuoteSpy = twainService.getQuote.and.returnValue( of(testQuote) );

Synchronous tests

https://angular.io/guide/testing#synchronous-tests

Async test with fakeAsync()

https://angular.io/guide/testing#async-test-with-fakeasync

The tick() function

Calling tick() simulates the passage of time until all pending asynchronous activities finish. In this case, it waits for the error handler's setTimeout();

Async observables

Async observable helpers

RxJS defer() returns an observable. It takes a factory function that returns either a promise or an observable. When something subscribes to defer's observable, it adds the subscriber to a new observable created with that factory.

RxJS defer() transform the Promise.resolve() into a new observable that, like HttpClient, emits once and completes. Subscribers will be unsubscribed after they receive the data value.

以下を用意

import { defer } from 'rxjs/observable/defer';

/** Create async observable that emits-once and completes
 *  after a JS engine turn */
export function asyncData<T>(data: T) {
  return defer(() => Promise.resolve(data));
}

/** Create async observable error that errors
 *  after a JS engine turn */
export function asyncError<T>(errorObject: T) {
  return defer(() => Promise.reject(errorObject));
}

More async tests

Async test with async()

https://angular.io/guide/testing#async-test-with-async

fakeではないasync

whenStable

The test must wait for the getQuote() observable to emit the next quote. Instead of calling tick(), it calls fixture.whenStable().

tickの代わりにfixture.whenStableを実行する必要がある

Jasmine done()

https://angular.io/guide/testing#jasmine-done

もちろん一般的な done も使える

But it is occasionally necessary. For example, you can't call async or fakeAsync when testing code that involves the intervalTimer() or the RxJS delay() operator.

Component marble tests

https://angular.io/guide/testing#component-marble-tests

Component with inputs and outputs

https://angular.io/guide/testing#component-with-inputs-and-outputs

@Input に対応

// ...

comp = fixture.componentInstance;

// ...

// mock the hero supplied by the parent component
expectedHero = { id: 42, name: 'Test Name' };

// simulate the parent setting the input property with that hero
comp.hero = expectedHero;

// trigger initial data binding
fixture.detectChanges();

Clicking

https://angular.io/guide/testing#clicking

Component inside a test host

https://angular.io/guide/testing#component-inside-a-test-host

  • Component with inputs and outputsと違うやり方。ホストのコンポーネントから操作する。
  • TestHostComponentを作ってその中で制御

Routing component

https://angular.io/guide/testing#routing-component

  • Routerをもっているコンポーネントのテスト
  • 「Routerのテスト」をする必要はないのでspyをつくる
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);

// ...

const spy = router.navigateByUrl as jasmine.Spy;
const navArgs = spy.calls.first().args[0];

Routed components

https://angular.io/guide/testing#routed-components

  • Router Navigation の遷移先
  • ActivatedRoute をどうテストするか

paramMap returns an Observable that can emit more than one value during a test. You need the router helper function, convertToParamMap(), to create a ParamMap. Other routed components tests need a test double for ActivatedRoute.

Nested component tests

https://angular.io/guide/testing#nested-component-tests

不要なコンポーネントをどう扱うか。

Stubbing unneeded components

@Component({selector: 'app-banner', template: ''})
class BannerStubComponent {}

TestBed.configureTestingModule({
  declarations: [
    AppComponent,
    BannerStubComponent
  ]
});

NO_ERRORS_SCHEMA

tells the Angular compiler to ignore unrecognized elements and attributes.

Use both techniques together

実際には両方のテクニックを利用するはず

https://angular.io/guide/testing#components-with-routerlink

RouterLinkのスタブを作成

@Directive({
  selector: '[routerLink]',
  host: { '(click)': 'onClick()' }
})
export class RouterLinkDirectiveStub {
  @Input('routerLink') linkParams: any;
  navigatedTo: any = null;
  onClick() {
    this.navigatedTo = this.linkParams;
  }
}

By.directive and injected directives

beforeEach(() => {
  fixture.detectChanges(); // trigger initial data binding

  // find DebugElements with an attached RouterLinkStubDirective
  linkDes = fixture.debugElement
    .queryAll(By.directive(RouterLinkDirectiveStub));

  // get attached link directive instances
  // using each DebugElement's injector
  routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub));
});

Use a page object

Setup with module imports

https://angular.io/guide/testing#setup-with-module-imports

モジュールインポートについて解説

One approach is to configure the testing module from the individual pieces as in this example:

  • Import a shared module でも可能
  • 実は自身を含む Feature module を読み込んでもおk

Override component providers

https://angular.io/guide/testing#override-component-providers

どうやってコンポーネントprovidersで登録されたサービスをおきかえるか


(すでに中途半端な内容になっていますが、一旦ここまで。ここからさらに、DirectiveやPipeのテストの説明、よくある質問などがAngular - Testingには記述されています。)

6月メモ・リンク集

6月に調べたことのメモです。

Angular 関連

ngrx 関連

ngrx 実装メモ

f:id:ryotah:20180706181849p:plain

store をつくるとき、どのような順で整理して実装しているか、についてのメモです。
例えば「ID順で表示される顧客一覧画面(テキストによる検索機能と追加読み込み機能)」をつくる場合は以下のように整理します。

  1. その機能 (Feature)で「できること」のリストアップ
    • 顧客一覧表示
    • 顧客検索
    • 一覧を追加読み込み
    • 検索条件を編集
    • 別画面(別 Feature)に移動
  2. 必要な情報は何か
    • 取得した顧客情報
    • 顧客一覧のローディング状況
    • 現在のページ番号
    • 検索条件
  3. 各 state を作成
    • customer-list.reducre.ts
      • customerList: Customer[];
      • loading: boolean
      • page: number
      • query: string
  4. action になりそうなものをリストアップ
    • 検索開始(検索条件が空の場合はID順で取得)
    • 検索成功
    • 検索失敗
    • 追加読み込み開始
    • 追加読み込み成功
    • 追加読み込み失敗
    • 検索条件を更新
    • ページを離れる
  5. action と reducer の関係を考える
    • 検索開始
      • loading = true
      • (その他のデータは一旦リセット)
    • 検索成功
      • customerList = 検索結果
      • loading = false
      • page = 1
    • 検索失敗
      • loading = false
    • 追加読み込み開始
      • loading = true
    • 追加読み込み成功
      • customerList += 検索結果
      • loading = false
      • page += 1
    • 追加読み込み失敗
      • loading = false
    • ページを離れる
      • データをリセット
  6. 無駄が多そうな場合調整する
    今回は「検索」と「追加読み込み」がほぼ同じなので共通化できそう

結果

Actions
// store/actions/customer-lits.actions.ts

export enum CustomerListActionTypes {
  LoadFirstList = '[CustomerList] Load First List',
  LoadList = '[CustomerList] Load List',
  LoadListFail = '[CustomerList] Load List Fail',
  LoadListSuccess = '[CustomerList] Load List Success',
  ResetList = '[CustomerList] Reset List',
  UpdateQueryFromParamMap = '[CustomerList] Update Query From Param Map',
}

export class LoadFirstList implements Action {
  readonly type = CustomerListActionTypes.LoadFirstList;
}

export class LoadList implements Action {
  readonly type = CustomerListActionTypes.LoadList;
}

export class LoadListFail implements Action {
  readonly type = CustomerListActionTypes.LoadListFail;
}

export class LoadListSuccess implements Action {
  readonly type = CustomerListActionTypes.LoadListSuccess;
  constructor(public payload: Customer[]) {}
}

export class ResetList implements Action {
  readonly type = CustomerListActionTypes.ResetList;
}

export class UpdateQueryFromParamMap implements Action {
  readonly type = CustomerListActionTypes.UpdateQueryFromParamMap;
  constructor(public payload: ParamMap) {}
}

export type CustomerListActions =
  | LoadFirstList
  | LoadList
  | LoadListFail
  | LoadListSuccess
  | ResetList
  | UpdateQueryFromParamMap;
Reducers
// store/reducers/customer-lits.reducer.ts

export interface State {
  customerList: Customer[];
  loading: boolean;
  page: number;
  query: string;
}

const initialState: State = {
  customerList: [],
  loading: false,
  page: 0,
  query: '',
};

export function reducer(
  state = initialState,
  action: CustomerListActions
): State {
  switch (action.type) {
    case CustomerListActionTypes.LoadList:
      return { ...state, loading: true };
    case CustomerListActionTypes.LoadListFail:
      return { ...state, loading: false };
    case CustomerListActionTypes.LoadListSuccess:
      return {
        ...state,
        customerList: [...state.customerList, ...action.payload],
        loading: false,
        page: state.page + 1,
      };
    case CustomerListActionTypes.ResetList:
      return initialState;
    case CustomerListActionTypes.UpdateQueryFromParamMap:
      return { ...state, query: action.payload.get('query') || '' };
    default:
      return state;
  }
}

export const getCustomerList = state => state.customerList;
export const getLoading = state => state.loading;
export const getPage = state => state.page;
export const getQuery = state => state.query;
Effects
// store/effects/customer-lits.effects.ts

@Injectable()
export class CustomerListEffects {
  @Effect()
  loadFirst$ = this.actions$.pipe(
    ofType<LoadFirstList>(CustomerActionTypes.LoadFirstList),
    mergeMap(action => [new ResetList(), new LoadList()])
  );

  @Effect()
  load$ = this.actions$.pipe(
    ofType<LoadList>(CustomerActionTypes.LoadList),
    withLatestFrom(
      this.store.select(fromStore.getQuery),
      this.store.select(fromStore.getPage)
    ),
    switchMap(([action, query, page]) =>
      this.service.getList(query, page + 1).pipe(
        map(customerList => new LoadListSuccess(customerList)),
        catchError(error => of(new LoadListFail()))
      )
    )
  );

  constructor(
    private actions$: Actions,
    private service: CustomerListService,
    private store: Store<fromStore.State>
  ) {}
}
Container
// コンテナコンポーネント

export class CustomerListContainer {

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    protected store: Store<fromStore.State>
  ) {

    route.paramMap.pipe(takeUntil(this.unsubscribe)).subscribe(queries => {
      this.store.dispatch(new UpdateQueryFromParamMap(queries));
      this.store.dispatch(new LoadFirstList());
    });

    // ...
}

CSS 関連

flex-basis」と「width」の違いは何でしょうか? 「flex-basis」は、flexコンテナの主軸の方向に相当するということです。

flex-basis」は2つとも同じ値です。つまり、十分なスペースがあれば(コンテナは600pxと余白とパディングの余裕があります)、どちらも幅が300pxになります。

しかし、コンテナが大きくなるにつれて、「.square#one」(flex-grow :2)は2倍の速さで広がります。逆に、コンテナが縮むにつれて、「.square#two」(flex-shrink :2)は2倍の速さで縮小します。

「.square#one」が広がる時に、「.square#two」の2倍のサイズには広がりません。同様に縮む時も1/2のサイズに縮むのではありません。

それぞれの値で指定した「2 1」、「1 2」はサイズではありません。広がる時と縮む時の率です。

5月メモ・リンク集

5 月に調べたことのメモです。

Angular 関連

third party lib

Once you import a library via the scripts array, you should not import it via a import statement in your TypeScript code (e.g. import * as $ from 'jquery';). If you do that you'll end up with two different copies of the library: one imported as a global library, and one imported as a module.

Testing in ngrx

// error
expect(action).toEqual({ type: LOAD_PIZZAS });
// => Expected object to be a kind of Object, but was LoadPizzas({ type: '[Products] Load Pizzas' })

// もうちょい
expect(action.type).toEqual(LOAD_PIZZAS);

// good
expect({ ...action }).toEqual({ type: LOAD_PIZZAS });

Reducers play a few key roles for us:

  • Accept old state, and an action
  • Respond to actions and compose/return new state
  • Handle changes via immutable patterns ... To go with it, my reducer - which uses an entity pattern to flatten my data structure into object keys for performance:

Version 6

Rx 関連

catchError

Rails 関連

double というメソッドを使うと、モックオブジェクトを作れます。 引数で渡す文字列は任意です。好きな文字列を渡しても構わないですし、省略することもできます。 ... RSpec では allow(モックオブジェクト).to receive(メソッド名) の形で、モックに呼び出し可能なメソッドを設定できます。

その他

type Foo = {
  name: string;
  age: number;
} & {
  [prop: string]: string;
};