AngularアプリケーションにSchematicsを導入し、コンポーネント量産体制を作る

Kana Otawara
nextbeat-engineering
17 min readOct 15, 2020

本記事の対象読者:Angularアプリケーション開発で、いろんなコンポーネントでなんか同じコードを書いている気がして面倒…でも、必要なものだし自動化できないかな?な人

こんにちは、エンジニアの太田原です。今回はAngularアプリケーションへのSchematicsへの導入について書いていきます。

Schematicsについて

Angularでは、Schematicsというコードジェネレーターの仕組みを使うことができます。

Angular CLIのng generateなどは、これを用いて実装されています。

例えば、ng generate component test を実行すると、以下のようなコンポーネントのファイルが生成されます。

import { Component, OnInit } from '@angular/core';@Component({
selector: 'app-test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
constructor() { } ngOnInit(): void {
}
}

このようなCLIで作成されるコンポーネントはある程度コマンドラインオプションでカスタマイズできるものの、例えば

  • importやDIでこれは入れておきたい
  • このプロパティは定義しておきたい
  • こういうコメントを入れておきたい

などのコンポーネントの内容に関する細かいカスタマイズには対応できません。

こういうことをしたくなってくると、カスタムテンプレートを作成し、自分でSchematicsの設定を書いてコードを生成するということになります。

やり方については既にQiitaなどで記事を書かれている方がいらっしゃいますが、私が見た限りでは独立したSchematicsプロジェクトを作成→npm公開、という内容が多い気がしました。

しかし、個人的には自分の担当プロダクトのコードに合わせてカスタムされたテンプレートがほしいと思っていました。つまり、

  • 公開する必要はない
  • プロダクトコードのAngularプロジェクトの一部としてSchematicsのテンプレートや設定ファイルを管理する形としたい

というケースです。よって、本記事では既存のAngularアプリケーションを補助する形としてSchematicsを導入していく方法について記載していきます。

サンプルコード

https://github.com/knts0/sample-app-using-schematics

src/app/user/ 配下にユーザー登録フォームのコンポーネントUserComponentがあるAngularアプリケーションです。

似たようなコンポーネントを量産したいという状況を想定しています。

Schematics関連のファイル作成

サンプルコードでは出来上がった状態になっていますが、sample-schematics/ 配下のファイルの作り方について説明していきます。

以下schematics-cliを使用していくので、まずグローバルインストールしておきます。

$ npm install -g @angular-devkit/schematics-cli

サンプルアプリのプロジェクトルートで

$ schematics blank sample-schematics

を実行すると、

sample-app-using-schematics(プロジェクトルート)
└ sample-schematics
 ├ node_modules/
 └ src/
  └ sample-schematics
   ├ index.ts
   └ index_spec.ts
  └ collection.json
 ├ .gitignore
 ├ .npmignore
 ├ package.json
 ├ package-lock.json
 ├ README.md
 └ tsconfig.json

のようにsample-schematics/ 配下にファイル群が生成されます。(これすらも自動生成できるというのが素晴らしいですね。)

構成について触れておくと、sample-schematics/ 配下が1つのnpmプロジェクトになっていて、package.jsonで重要なところを抜粋すると、

{
...
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "npm run build && jasmine src/**/*_spec.js"
},
...
"schematics": "./src/collection.json",
...
}

のようにbuildとtestの設定、Schematicsのスキーマファイルの指定が行われています。

スキーマファイルとはなんぞや、というのは後ほど説明します。

テンプレートファイルを作成

sample-schematics/src/sample-schematics/files/ にテンプレートファイルを配置します。

コンポーネント周りのファイル、モデル、サービスの一式を定義しています。

「name」という変数を指定しており、任意の値を埋め込むことができます。

__name@dasherize__.ts
__name@dasherize__.component.html
__name@dasherize__.component.scss
__name@dasherize__.component.ts
__name@dasherize__.service.ts

今回はフォームのコンポーネントのテンプレートを作りたいという設定なので、テンプレートファイルの内容はUserComponentの内容を一般化したものです。

例えば、__name@dasherize__.component.ts はフォーム周りやサービスへの依存や、必ず必要になるであろうonSubmitの実装を記載しています。

@angular/formsからのimportについても、様々なフォームを想定し、一通りの指定をしています。実際のFormGroupの内容については開発するタイミングで埋めてもらうようにしています。

dasherizeやclassifyなどは変数nameのフォーマットを変換するものです。dasherize:ハイフン区切りに、classify:クラス名などに用いられる大文字始まりに変換してくれます。こういった関数は、@angular-devkit/core に含まれるものを内部で使っているようです。他にも様々な形式へ変換できます。参考:angular-cli/strings.ts at master · angular/angular-cli

import { Component, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'
import { <%= classify(name) %>Service } from './<%= dasherize(name) %>.service'
@Component({
selector: '<%= dasherize(name) %>',
templateUrl: './<%= dasherize(name) %>.component.html',
styleUrls: ['./<%= dasherize(name) %>.component.scss']
})
export class <%= classify(name) %>Component implements OnInit {
form: FormGroup constructor(
private fb: FormBuilder,
private <%= camelize(name) %>Service: <%= classify(name) %>Service,
) { }
ngOnInit(): void {
this.form = this.fb.group({
// please implement
})
}
onSubmit(): void {
this.<%= name %>Service.create(this.form.value)
}
}

テンプレートファイルは以下のような感じで、サンプルの実装が入っているのみでフォームの内容は開発するタイミングで埋めてもらいます。

<form [formGroup]="form">
<div class="form-table">
<div class="form-table__row">
<div class="label">sample</div>
<div class="content">
<input formControlName="sample" type="text">
</div>
</div>
</div>

<div>
<button [disabled]="form.invalid" (click)="onSubmit()">送信</button>
</div>
</form>

テンプレートファイルからコード生成する設定を行う

sample-schematics/src/sample-schematics/index.ts にコード生成の設定を書いていきます。

記述の方法は、Schematicsの公式リファレンスの「Templating」の章や、ng generate component内部実装が参考になります。

簡単にコードの説明をしていきます。

コード生成は前述のschematics-cliで行いますが、その際のコマンドライン引数が_optionsに渡されてきます。

  • path:任意のコード生成先を指定できる
  • name:テンプレートの変数nameに埋め込む値

sampleSchematics関数のreturn部分で、ルール(Rule型)を設定します。Treeは、仮想ファイルシステムのようなものと考えて良さそうです。ここでは、実際のAngularのプロジェクトディレクトリを表現した仮想ファイルシステムが渡されてくるので、それに対してどんな変更を加えるかの定義をしていくというイメージです。

(ルールの一覧は、https://github.com/angular/angular-cli/tree/master/packages/angular_devkit/schematics#provided-rules を参照ください。)

return部分のコードは、Schematicsの公式リファレンスの「Templating」の章を参考にしました。肝になるのは

  • テンプレートファイルからコード生成(template 関数)
  • move 関数によってコマンドラインで指定した場所に移動

の部分です。

import { 
Path,
normalize,
strings,
} from '@angular-devkit/core';
import {
Rule,
SchematicContext,
apply,
branchAndMerge,
mergeWith,
move,
template,
url,
} from '@angular-devkit/schematics';
// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function sampleSchematics(_options: any): Rule {
return (_, _context: SchematicContext) => {
// コードを生成するパスの指定
const pathToCreate: Path = normalize(_options.path as string)
return branchAndMerge(
mergeWith(apply(url('./files'), [
template({
...strings,
name: _options.name,
}),
move(pathToCreate),
]))
);
};
}

更に、sample-schematics/collection.json についても見ていきます。

これはコードの生成を行う際にコマンドライン(schematics-cli)で指定する「Schematic」を定義するものです。(詳しくは:Angular 日本語ドキュメンテーション — Schematics の作成

今回は以下のように設定し「sample-schematics」というSchematicを定義し、前述のsample-schematics/src/sample-schematics/index.ts に関連付けています。

{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"sample-schematics": {
"description": "generates form component",
"factory": "./sample-schematics/index#sampleSchematics"
}
}
}

テスト

テンプレートからの自動生成が正しくできているかの検証もできます。

ユニットテストのコード(Jasmineで記述)がsample-schematics/src/sample-schematics/index_spec.ts として生成されているので、こちらを編集していきます。

今回は以下のようにしてみました。

  • 正常系:コマンドライン引数の複数のバリエーションに対し、出力されるファイルのリストと、中身の一部(特に埋め込み変数のあたり)を検証
  • 異常系:コマンドライン引数が指定されないと失敗するのを確認

sample-schematics/ 直下で

$ npm run test

を実行し、テストが行なえます。

コード生成

ここからは実際にコードの生成をしていきます。

事前にsample-schematics/ 配下のものをビルドしておく必要があります。

$ cd sample-schematics
$ npm run build

そして、アプリケーションのルートディレクトリに移動し、schematics-cliによりコード生成を行います。

--nameにはテンプレートのname変数に埋め込まれる値を指定し、--pathにはコードの生成先ディレクトリを指定します(相対パス)

$ cd ../
$ schematics ./sample-schematics/src/collection.json:sample-schematics --name=test --path=src/app/test --dry-run=false
CREATE src/app/test/test.component.html (341 bytes)
CREATE src/app/test/test.component.scss (151 bytes)
CREATE src/app/test/test.component.ts (626 bytes)
CREATE src/app/test/test.service.ts (191 bytes)
CREATE src/app/test/test.ts (41 bytes)

毎回長いコマンドを打つのは大変なので、

npmのscriptsに定義したり・・・

"scripts": {
...
"schematics.form": "schematics ./sample-schematics/src/collection.json:sample-schematics --dry-run=false"
},
npm run schematics.form -- —-name=test --path=src/app/test

npm linkをつかって、アプリケーションそのもののnpmプロジェクトと、Schematicsのnpmプロジェクト(sample-schematics/)を関連付けてng generateで呼び出すということもできます。

cd sample-schematics/
npm link
cd ../
npm link sample-schematics
# 1番目のsample-schematicsはnpm linkした「sample-schematics」のnpmプロジェクトと言う意味、
# 2番目のsample-schematicsはcollection.jsonで指定したSchema名としての「sample-schematics」
ng generate sample-schematics:sample-schematics

無事、src/app/test/ 配下にファイルが生成されています。

import { Component, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'
import { TestService } from './test.service'
@Component({
selector: 'test',
templateUrl: './test.component.html',
styleUrls: ['./test.component.scss']
})
export class TestComponent implements OnInit {
form: FormGroup constructor(
private fb: FormBuilder,
private testService: TestService,
) { }
ngOnInit(): void {
this.form = this.fb.group({
// please implement
})
}
onSubmit(): void {
this.testService.create(this.form.value)
}
}

必要なimportやDIはされている状態なので、あとはコンポーネント固有のロジックを書いていくだけです✨

Customize more…

⭐他にも自動生成の設定を増やしたい:テンプレートファイルや生成ロジックを記述し、collection.jsonに追加

⭐コンポーネント生成だけではなくNgModuleへの宣言追加もしたい:ng generate component内部実装 を参考にする

などができそうです。

感想

テストができるので、気兼ねなくテンプレートファイルを進化させていくこともできそうで良いと思いました。

また、意外と日本語の情報が少なかったり、公式のリファレンスがあっさりしているのでなかなか自分のやりたいことをやるのが難しかったです。

参考

Angular 日本語ドキュメンテーション — Schematics を使用したコード生成

angular-cli/README.md at master · angular/angular-cli

Schematicsを作ってみよう — Qiita

実践!Schematics — Qiita

Angular Schematics: Unit Testing. Building Schematics can be magical… | by Jonathan Campos | Angular Blog

--

--