LoginSignup
6
3

More than 5 years have passed since last update.

angular.io Guide: Pipes

Posted at

これは、Angular の公式ドキュメントの Pipes の章 を意訳したものです。
駆け足で翻訳したので至らない点もありますが、あしからずご承知おきください。

バージョン 4.3.1 のドキュメントをベースにしています。

Pipes

すべてのアプリケーションは、データを取得し、整形して、ユーザーに表示するという、一見シンプルなタスクのように見えます。データの取得は、ローカル変数の作成やWebSocketを経由したデータのストリーミングなどのシンプルな操作で行うことができます。

データが受け取ったら、生のtoString値を直接Viewにプッシュすることもできますが、これはユーザーにとって良いUXとはほぼ言えないでしょう。

よくある一例として、生の文字列形式の日付があったとします。画面上には Fri Apr 15 1988 00:00:00 GMT-0700 (太平洋夏時間)ではなく、1988年4月15日 のような、ユーザーにとってシンプルなフォーマットで日付を表示したくなりますよね。

この一例のように、明らかに一部の値には編集(整形)して表示するメリットがあります。同じような変換処理の多くが、多くのアプリケーションの内外で同じように繰り返されていることに気付くかもしれません。

これは一貫性のあるスタイルを適用していると考えることができます。実際には、スタイルを行うときにHTMLテンプレートで適用したいと思うかもしれません。

そこで今回は、HTMLテンプレートで宣言して表示値を変換するロジックを書く方法、Angular pipesを紹介します。

ここからサンプル(ライブサンプル/ダウンロード)ができます。

Pipeの使い方

Pipeは入力したいデータをInputして、それを希望のOutputに変換します。このページでは、Pipeを使用してコンポーネントの誕生日プロパティを人に優しい(読みやすい)日付フォーマットに変換します。

src/app/hero-birthday1.component.ts

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

@Component({
  selector: 'hero-birthday',
  template: `<p>The hero's birthday is {{ birthday | date }}</p>`
})
export class HeroBirthdayComponent {
  birthday = new Date(1988, 3, 15); // April 15, 1988
}

コンポーネントのテンプレートに注目してください。

src/app/app.component.html

<p>The hero's birthday is {{ birthday | date }}</p>

補間式 (interpolation expression) の中では、コンポーネントの誕生日の値をパイプ演算子 (|) を通して右側の日付Pipe関数に渡します。すべてのPipeはこのかたちで動作します。

日付と通貨パイプには、ECMAScript Internationalization APIが必要です。 Safariとその他古いブラウザではサポートされていません。polyfillでサポートを追加することができます。

<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script>

ビルトイン Pipe

Angularには、DatePipe、UpperCasePipe、LowerCasePipe、CurrencyPipe、PercentPipeといったPipeが用意されています。これらは、すべてテンプレートで使用できます。

APIリファレンス の pipes トピックで、これらおよびその他の組み込みパイプの詳細を参照できます。単語 "pipe" を含むエントリをフィルタリングしてください。
Angularには、FilterPipeとOrderByPipeはありません。(このページの付録セクションで理由を説明します)

Pipe のパラメータ化

パイプは、任意の数のオプションパラメータを受け入れて、その出力を微調整することができます。パイプにパラメータを追加するには、パイプ名の後にコロン:を付け、パラメータ値(通貨:'EUR'など)を入力します。パイプが複数のパラメータを受け入れる場合は、値をコロン:で区切ります(slice:1:5など)

誕生日テンプレートを変更して、日付パイプに書式パラメータを指定します。ヒーローの4月15日の誕生日をフォーマットした後、それは04/15/88としてレンダリングされます:

src/app/app.component.html

<p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p>

パラメータ値には、文字列リテラルやコンポーネントプロパティなど、任意の有効なテンプレート式を使用できます([テンプレート構文]ページの[テンプレート式]セクションを参照)。

つまり、バインディングを介して誕生日の値を制御するのと同じ方法で、バインディングを通じてフォーマットを制御できます。

パイプのformatパラメータをコンポーネントのformatプロパティにバインドする2番目のコンポーネントを記述します。そのコンポーネントのテンプレートは次のとおりです。

src/app/hero-birthday2.component.ts (template)

template: `
  <p>The hero's birthday is {{ birthday | date:format }}</p>
  <button (click)="toggleFormat()">Toggle Format</button>
`

また、テンプレートにボタンを追加し、clickイベントをコンポーネントのtoggleFormat()メソッドにバインドしました。このメソッドは、コンポーネントのformatプロパティを短いフォーム('shortDate')と長いフォーム('fullDate')の間で切り替えます。

src/app/hero-birthday2.component.ts (class)

export class HeroBirthday2Component {
  birthday = new Date(1988, 3, 15); // April 15, 1988
  toggle = true; // start with true == shortDate

  get format()   { return this.toggle ? 'shortDate' : 'fullDate'; }
  toggleFormat() { this.toggle = !this.toggle; }
}

ボタンをクリックすると、表示された日付は「04/15/1988」と「Friday, April 15, 1988」の間で切り替わります。

Date Format Toggle

日付パイプAPIリファレンス」ページのDatePipeフォーマットオプションの詳細を参照してください。

Pipe のチェーン

潜在的に有用な組み合わせでPipeを連鎖させて処理させることもできます。次の例では、誕生日を大文字で表示するために、誕生日はDatePipeに連鎖され、続いてUpperCasePipeに連鎖されます。誕生日はAPR 15, 1988に表示されます。

src/app/app.component.html

The chained hero's birthday is
{{ birthday | date | uppercase}}

FRIDAY, APRIL 15, 1988 と表示したい場合は、上記と同じパイプを連鎖させますが、日付にもパラメータを渡します。

src/app/app.component.html

The chained hero's birthday is
{{  birthday | date:'fullDate' | uppercase}}

カスタム Pipe

独自のカスタムパイプを作成することもできます。ヒーローのPowerを高めることができる ExponentialStrengthPipe というカスタムパイプがあります:

src/app/exponential-strength.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';
/*
 * Raise the value exponentially
 * Takes an exponent argument that defaults to 1.
 * Usage:
 *   value | exponentialStrength:exponent
 * Example:
 *   {{ 2 | exponentialStrength:10 }}
 *   formats to: 1024
*/
@Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform {
  transform(value: number, exponent: string): number {
    let exp = parseFloat(exponent);
    return Math.pow(value, isNaN(exp) ? 1 : exp);
  }
}

このパイプ定義は、以下の重要な点を明らかにします:

  • パイプは、パイプメタデータで装飾されたクラスです。
  • パイプクラスは、入力値の後にオプションのパラメータを受け入れ、変換された値を返すPipeTransformインタフェースのtransformメソッドを実装します。
  • パイプに渡される各パラメータの変換メソッドには、さらに1つの引数があります。パイプには指数のようなパラメータが1つあります。
  • これがパイプであることをAngularに伝えるには、コアのAngularライブラリからインポートする@Pipeデコレータを適用します。
  • @Pipeデコレータでは、テンプレート式内で使用するパイプ名を定義することができます。有効なJavaScript識別子である必要があります。あなたのパイプの名前は指数関数です。

PipeTransform interface について

変換メソッドはパイプにとって不可欠です。PipeTransformインタフェースは、そのメソッドを定義し、ツーリングとコンパイラの両方をガイドします。技術的にはオプションです。
Angularは、transform メソッドを無関係に探して実行します。

パイプをデモするためのコンポーネントが必要になりましたので、記述していきましょう。

src/app/power-booster.component.ts

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

@Component({
  selector: 'power-booster',
  template: `
    <h2>Power Booster</h2>
    <p>Super power boost: {{2 | exponentialStrength: 10}}</p>
  `
})
export class PowerBoosterComponent { }

Power Booster

次の点に注意してください。

  • カスタムパイプは、組み込みパイプと同じ方法で使用します。
  • AppModuleの declarations 配列にパイプを含める必要があります。

REMEMBER THE DECLARATIONS ARRAY

カスタムパイプを手動で登録する必要があります。そうしないと、Angularはエラーを報告します。前の例では、Angularビルトインパイプはすべて事前登録されているため、DatePipeはリストされませんでした。

ライブサンプルダウンロードのサンプルで動作を調べることができます。テンプレートの値とオプションの指数を変更してみてください。

Power Boost Calculator

カスタムパイプをテストするためにテンプレートをアップデートするのは楽しいことではありません。この例を、パイプと双方向データバインディングをngModelと組み合わせた「Power Boost Calculator」にアップグレードして見ましょう。

src/app/power-boost-calculator.component.ts

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

@Component({
  selector: 'power-boost-calculator',
  template: `
    <h2>Power Boost Calculator</h2>
    <div>Normal power: <input [(ngModel)]="power"></div>
    <div>Boost factor: <input [(ngModel)]="factor"></div>
    <p>
      Super Hero Power: {{power | exponentialStrength: factor}}
    </p>
  `
})
export class PowerBoostCalculatorComponent {
  power = 5;
  factor = 1;
}

Power Boost Calculator

Pipes と change detection

Angularは、すべてのDOMイベントの後に実行されるChange-Detectionプロセス(キーストローク、マウス移動、タイマーティック、およびサーバーレスポンス)ごとにデータバインド値の変更を探します。これはコストの高い処理になる可能性があるので、Angularは可能な限りコストが適切に下がるように努めています。

Angular を使用すると、パイプを使用するときに、より簡単で迅速な Change-Detection アルゴリズムが選択されます。

Pipeを使わずに記述した場合

次の例では、コンポーネントはデフォルトのアグレッシブなChange-Detection 戦略を使用して、heroes 配列のすべてのヒーローの表示を監視および更新します。テンプレートは次のとおりです。

src/app/flying-heroes.component.html (v1)

New hero:
  <input type="text" #box
          (keyup.enter)="addHero(box.value); box.value=''"
          placeholder="hero name">
  <button (click)="reset()">Reset</button>
  <div *ngFor="let hero of heroes">
    {{hero.name}}
  </div>

コンパニオンコンポーネントクラスはヒーローを提供し、heroes を配列に追加し、配列をリセットすることができます。

src/app/flying-heroes.component.ts (v1)

export class FlyingHeroesComponent {
  heroes: any[] = [];
  canFly = true;
  constructor() { this.reset(); }

  addHero(name: string) {
    name = name.trim();
    if (!name) { return; }
    let hero = {name, canFly: this.canFly};
    this.heroes.push(hero);
  }

  reset() { this.heroes = HEROES.slice(); }
}

Heroを追加して、Angularを更新して表示することができます。リセットボタンをクリックすると、Angularはheroesを元のheroesの新しい配列に置き換えて表示を更新します。heroを削除または変更する機能を追加した場合、Angularはこれらの変更を検出して表示を更新します。

FlyingHeroesPipe

飛行可能なヒーローだけにヒーローのリストをフィルターする *ngFor リピーターに FlyingHeroesPipe を追加します。

src/app/flying-heroes.component.html (flyers)

<div *ngFor="let hero of (heroes | flyingHeroes)">
  {{hero.name}}
</div>

FlyingHeroesPipe の実装は、先に説明したカスタムパイプのパターンに従います。

src/app/flying-heroes.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

import { Flyer } from './heroes';

@Pipe({ name: 'flyingHeroes' })
export class FlyingHeroesPipe implements PipeTransform {
  transform(allHeroes: Flyer[]) {
    return allHeroes.filter(hero => hero.canFly);
  }
}

ライブサンプル/ダウンロードの例では、奇妙な動作に注目してください。飛行できるヒーローを追加しても、"Heroes who fly"の一覧に追加したヒーローが表示されません。

あなたが望む振る舞いを得られていないにもかかわらず、Angularは壊れません。これは、リストやその項目の変更を無視する“異なるChange-Detectionアルゴリズム”が使われているためです。

ヒーローがどのように追加されたかに注目してください:

src/app/flying-heroes.component.ts

this.heroes.push(hero);

ヒーローを heroes の配列に追加します。配列への参照は変更されていません。それは同じ配列です。それはすべてAngularの心配です。その観点からは、同じ配列、変更なし、表示更新はありません。

それを修正するには、新しいヒーローが追加された配列を作成し、ヒーローに割り当てます。今回、Angularは配列参照が変更されたことを検出します。パイプを実行し、新しいフライングヒーローを含む新しい配列で表示を更新します。

配列を変更すると、パイプは呼び出されず、表示は更新されません。配列を置き換えると、パイプが実行され、表示が更新されます。Flying Heroesアプリケーションは、チェックボックススイッチと追加のディスプレイでコードを拡張し、これらのエフェクトを体験するのに役立ちます。

Flying Heroes

配列を置き換えると、Angularにシグナルを送信して表示を更新することができます。あなたはいつ配列を交換するのですか?データが変更されたとき。この例では、データを変更する唯一の方法は主人公を追加するという簡単なルールです。

多くの場合、データがいつ変更されたかはわかりません。特に、遠方にあるアプリケーションの場所など、さまざまな方法でデータを変更するアプリケーションではそうです。そのようなアプリケーションのコンポーネントは、通常、その変更について知ることができません。

さらに、パイプを収容するためにComponentの設計を歪めることは賢明ではありません。コンポーネントクラスをHTMLから独立させておくように努めましょう。コンポーネントはパイプを認識していないはずです。

フライングヒーローだけでフィルタリングするには、不純なパイプの使用を検討してください。

純粋なPipeと不純なPipe (Pure and impure pipes)

パイプには純粋なものと不純なものがあります。パイプはデフォルトで純粋(pure)になっています。
これまでに見たパイプはすべて純粋です。pure フラグをfalseに設定することによって、パイプを不純にします。あなたは次のようにFlyingHeroesPipeを不純にすることができます:

src/app/flying-heroes.pipe.ts

@Pipe({
  name: 'flyingHeroesImpure',
  pure: false
})

まずはじめに、純粋なものと純粋でないPipeの違いを理解し、純粋なパイプから検討してください。

Pure pipes(純粋なPipe)

Angularは、入力値の純粋な変化を検出した場合にのみ、純粋なパイプを実行します。純粋な変更は、プリミティブ入力値(String、Number、Boolean、Symbol)または変更されたオブジェクト参照(Date、Array、Function、Object)への変更です。

Angularは(複合)オブジェクト内の変更は無視します。入力月を変更したり、入力配列に追加したり、入力オブジェクトのプロパティを更新すると、純粋なパイプは呼び出されません。

これは制限的なように見えるかもしれませんが、高速です。オブジェクト参照チェックは、差異の深いチェックよりもはるかに高速です。そのため、Angularはパイプの実行とビューの更新の両方をスキップできるかどうかを迅速に判断できます。

このため、Change-Detection方式を使用する場合は、純粋なパイプが適しています。できない場合は、不純なパイプを使用することができます。

あるいは、パイプをまったく使用しないのも一手かもしれません。コンポーネントのプロパティでPipeでやろうとしていた実装を行った方が良いかもしれません。この点については、このページの後半で説明します。

Impure pipes(不純なパイプ)

Angularは、ComponentのChange-Detectionサイクルが実行される度に不純なパイプを実行します。不純なパイプは、すべてのキーストロークやマウスの移動と同じくらい頻繁に呼び出されます。

この懸念点を念頭に置いて、細心の注意を払って不純なパイプを実装してください。処理コストが高く、処理に時間のかかるパイプは、UX(ユーザー体験)を著しく悪化させる可能性があります。

不純なFlyingHeroesPipe

スイッチのフリップがFlyingHeroesPipeFlyingHeroesImpurePipeに変えます。完全な実装は次のとおりです。

FlyingHeroesImpurePipe

@Pipe({
  name: 'flyingHeroesImpure',
  pure: false
})
export class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}

FlyingHeroesPipe

import { Pipe, PipeTransform } from '@angular/core';

import { Flyer } from './heroes';

@Pipe({ name: 'flyingHeroes' })
export class FlyingHeroesPipe implements PipeTransform {
  transform(allHeroes: Flyer[]) {
    return allHeroes.filter(hero => hero.canFly);
  }
}

あなたはFlyingHeroesPipeから継承し、内部的に何も変更されていないことを証明します。唯一の違いは、パイプメタデータの pure フラグです。

これは、transform関数が軽微で高速なものであるため、不純なパイプの良い候補です。

src/app/flying-heroes.pipe.ts (filter)

return allHeroes.filter(hero => hero.canFly);

FlyingHeroesComponentからFlyingHeroesImpureComponentを派生させることができます。

src/app/flying-heroes-impure.component.html (抜粋)

<div *ngFor="let hero of (heroes | flyingHeroesImpure)">
  {{hero.name}}
</div>

唯一の実質的な変更は、テンプレート内のパイプです。ライブサンプルダウンロードで確認してみてください。 heroes 配列を変更した場合でも、flying heroを追加すると flying heroes に更新が表示されます。

不純なAsyncPipe

Angular AsyncPipeは、不純なパイプの興味深い例です。 AsyncPipeは、PromiseまたはObservableを入力として受け取り、入力に自動的にサブスクライブし、最終的に出力された値を返します。

AsyncPipeもステートフルです。パイプは入力Observableへのサブスクリプションを維持し、Observableから到着時に値を引き渡します。

次の例では、Observableのメッセージ文字列(message$)をasyncパイプのビューにバインドします。

src/app/hero-async-message.component.ts

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

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';

@Component({
  selector: 'hero-message',
  template: `
    <h2>Async Hero Message and AsyncPipe</h2>
    <p>Message: {{ message$ | async }}</p>
    <button (click)="resend()">Resend</button>`,
})
export class HeroAsyncMessageComponent {
  message$: Observable<string>;

  private messages = [
    'You are my hero!',
    'You are the best hero!',
    'Will you be my hero?'
  ];

  constructor() { this.resend(); }

  resend() {
    this.message$ = Observable.interval(500)
      .map(i => this.messages[i])
      .take(this.messages.length);
  }
}

非同期パイプは、コンポーネントコード内に定型文(boilerplate)を保存します。コンポーネントは、非同期データソースを購読し、解決された値を抽出し、バインディングのために公開する必要はなく、破棄されたときに購読を解除する必要があります(強力なメモリリークの原因)。

不純なキャッシングパイプ

もう1つの不純なPipe、つまりHTTP要求を行うPipeを作成します。
不純なパイプは数ミリ秒ごとに呼び出されることに注意してください。あなたが慎重でない場合、このパイプのリクエストでサーバに大きく負荷をかけることになるでしょう。

次のコードでは、パイプはリクエストURLが変更されたときにのみサーバーを呼び出し、サーバーの応答をキャッシュします。このコードでは、Angular httpクライアントを使用してデータを取得しています。

src/app/fetch-json.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';
import { Http }                from '@angular/http';

import 'rxjs/add/operator/map';

@Pipe({
  name: 'fetch',
  pure: false
})
export class FetchJsonPipe  implements PipeTransform {
  private cachedData: any = null;
  private cachedUrl = '';

  constructor(private http: Http) { }

  transform(url: string): any {
    if (url !== this.cachedUrl) {
      this.cachedData = null;
      this.cachedUrl = url;
      this.http.get(url)
        .map( result => result.json() )
        .subscribe( result => this.cachedData = result );
    }

    return this.cachedData;
  }
}

今度は、テンプレートがこのパイプへの2つのバインディングを定義するハーネスコンポーネントでそれを実演し、両方ともheroes.jsonファイルからヒーローを要求します。

src/app/hero-list.component.ts

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

@Component({
  selector: 'hero-list',
  template: `
    <h2>Heroes from JSON File</h2>

    <div *ngFor="let hero of ('heroes.json' | fetch) ">
      {{hero.name}}
    </div>

    <p>Heroes as JSON:
      {{'heroes.json' | fetch | json}}
    </p>`
})
export class HeroListComponent { }

コンポーネントは次のようにレンダリングされます:

Hero List

パイプのデータ要求のブレークポイントには、次の情報が表示されます。

  • 各バインディングは、独自のパイプインスタンスを取得します。
  • 各パイプインスタンスは、独自のURLとデータをキャッシュします。
  • 各パイプインスタンスはサーバーを1回だけ呼び出します。

JsonPipe

前のコードサンプルでは、​​2番目のフェッチパイプバインディングは、より多くのパイプチェーニングを示しています。組み込みのJsonPipeにチェーンすることによって、JSON形式の同じヒーローデータを表示します。

JSONパイプでデバッグしよう

JsonPipeは、不思議に思われないデータバインディングを診断したり、将来のバインドのためにオブジェクトを検査できる簡単な方法として提供しています。

Pure pipes と pure functions

純粋なパイプは純粋な関数を使用します。純粋な関数は、検出可能な副作用なしに入力と戻り値を処理します。同じ入力を与えられた場合、常に同じ出力を返す必要があります。

このページの前半で説明したパイプは、純粋な機能で実装されています。組み込みのDatePipeは純関数実装の純粋なパイプです。
ExponentialStrengthPipeFlyingHeroesPipeもそうです。 FlyingHeroesImpurePipeをレビューしました。純粋な機能を備えた純粋でないパイプです。

しかし、常に純粋な関数を持つ純粋なパイプを実装してください。そうでなければ、チェックされた後に変更された式に関する多くのコンソールエラーが表示されます。

次のステップ

パイプは、共通の表示値変換をカプセル化して共有するための優れた方法です。それらのスタイルをスタイルのように使用し、テンプレートの表現にドロップして、ビューの魅力と利便性を高めることができます。

Angularに内蔵されているPipeのラインアップについては、APIリファレンスを参照してください。ゆくゆくはカスタムパイプを作成して、コミュニティに提供してみてくださいね。

付録: FilterPipe or OrderByPipe

Angularはリストのフィルタリングや並べ替えのためのパイプを提供しません。AngularJSを熟知している開発者は、これらをフィルターをfilterorderByとして認識しているかと思います。これらはのフィルターは、Angularにおいて相当するものはありません。

これは見落としではありません。Angularは、パフォーマンスが悪くなったり、積極的な小型化を妨げるようなパイプは提供しません。

filterorderByの両方には、オブジェクトのプロパティを参照するパラメータが必要です。このページの前半では、このようなパイプは不純であることが必要であり、ほぼすべての変更検出サイクルでAngularコールがPipeを汚染することが分かりました。

フィルタリング、特にソートは高コストな操作です。
Angularがこれらのパイプメソッドを毎秒何度も呼び出すと、ユーザーエクスペリエンスが中程度の規模の一覧でもひどく低下します。
filterorderByはAngularJSアプリで悪用される(誤った使い方をされる)ことが多く、それが原因でAngular自体が遅いという苦情があります。
AngularJSはfilterorderByを初期の頃から提供した結果、このパフォーマンス・トラップを作成したという間接的な意味では、この苦情はごもっともな意見です。

軽視の危険性は、それほど明白でない場合でも説得力があります。ヒーローのリストに適用されたソートパイプを想像してみてください。このリストは、主人公の名前と惑星の原点のプロパティによって次のようにソートされます。

<!-- NOT REAL CODE! -->
<div *ngFor="let hero of heroes | orderBy:'name,planet'"></div>

並べ替えフィールドをテキスト文字列で指定すると、パイプがインデックス(たとえばhero['name']など)によってプロパティ値を参照することが期待されます。残念ながら、積極的な縮小は、Hero.nameHero.planetHero.aHero.bのようになるようにHeroプロパティ名を操作します。明らかに hero['name'] は動作しません。

いくつかのケースでは積極的に縮小する気にはならないかもしれませんが、Angularプロダクトの積極的な小型化を妨げてはいけません。
したがって、AngularチームはAngularが提供するすべてのものが安全に細分化されるべきと判断しました。

Angularチームと経験豊富なAngular開発者の立場からは、フィルタリングと並べ替えロジックは、コンポーネント自身に実装することを強くお勧めします。コンポーネントはfilteredHeroesまたはsortedHeroesプロパティを作って公開し、サポートロジックを実行するタイミングと頻度を制御するようにします。
パイプに入れてアプリ全体で共有する機能は、フィルタリング/並べ替えサービスに書き込んでコンポーネントに注入することができます。

これらのパフォーマンスと最小化の考慮事項があなたには当てはまらない場合、あなたはいつでも独自のパイプを作るか(FlyingHeroesPipeのアプローチに似ています)、コミュニティで探すと良いでしょう。

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3