[入門] Sails.js v1.0 + Angular5でSPAをやっていく

はじめに

この記事は

Sails.jsでサーバーサイド、Angular5でフロントエンドを作り、
シンプルなSPAを作るサイクルを回せる状態にする方法を紹介する記事です。
話題としてはほぼ環境構築のみです。

対象者

node.jsを使ったことが無いことはない人むけ

Sails.jsとは

その名の通り、Rails風にMVCのWEBアプリを作れるNode.jsのフレームワーク。
サーバサイドを担当。
中身はexpressをラップしてるっぽい。

Angularとは

Google製のフロントエンドのJSのフレームワーク。
typescriptがサポートされておりやっていきやすい

環境

OS : windows10
node.js : v9.8.0
yarn : 1.5.1
@angular/cli : 1.7.4
@angular/core : 5.2.10
sails : 1.0.0

参考にした記事

http://fullstackengine.net/creating-a-new-project-with-angular-4-and-sails-js/

はじめる前に

以下のステップは単にインストールなのですでに入れてる人は読み飛ばしてOKです。

yarn

yarnはnpmと互換性のあるパッケージ管理ソフトです。
yarn.lockというロックファイルにより、インストールしたバージョンを厳密に明示できるため、
ある環境でできたことが、他の環境でも必ずできることを保証できます。

いろいろ便利なので入れときましょう。

https://yarnpkg.com/lang/ja/

chocolatey https://chocolatey.org/ が入っている場合は、以下のコマンドで入れられます。

choco install yarn

@angular/cli

@angular/cliは、Angularでの開発を楽にしてくれるクライアントツールです。
特にビルドやコンポーネントの生成、モックサーバーの立ち上げなど、本来素手でやるといろいろ設定をしなくてはいけないところを
(おそらく)ベストプラクティスでやってくれる優れものです。

基本的に本記事はこれに大きく寄りかかる形になるため、入れましょう。

yarn global add @angular/cli

Sails.js

入れましょう。

yarn global add sails

postman

APIを叩くのに便利なchrome 拡張です。
chrome web storeから入手できるので手に入れておきましょう。
curlでもOKです。

Angularをはじめる

ng new する

Angularの開発は基本的には先程入れた@angular/cliを利用します。
@angular/cliのコマンドは「ng」です。

さっそくやっていきましょう。

cd ルートにしたいフォルダ
ng new qiita_angular

「qiita_angular」の部分は適当で大丈夫です。

ここで作ったフォルダはいずれ用済みになるので、真剣に考える必要はありません。

すると、以下のようなフォルダができます。

└─qiita_angular
    ├─e2e
    ├─node_modules
    └─src
        ├─app
        ├─assets
        └─environments

このうち、ソースとして触る部分はsrc/appの中がメインとなります。

とりあえずdev-serverを立ち上げてみる

AngularのコンポーネントはテンプレートとなるHTMLファイルとそれを管理するtsファイルとに分かれています。
とりあえずAngularのコンポーネントが表示されることを確認したいので、
src/app/app.component.htmlを見てみましょう。

<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
...

こんな感じになっていると思います。
ここでいう「title」に、src/app/app.component.tsの「title」が入ってくるわけなので、
ためしにここの値を「app」から「qiita」に変えてみましょう。

export class AppComponent {
  title = 'app';
}

だったのを

export class AppComponent {
  title = 'qiita';
}

に変えた状態で、きちんとコンポーネントが表示されるよねというのを確認してみます。

@angular/cliでAngularのアプリを表示させる場合、
以下のコマンドを使います。

ng serve

以下のメッセージが出てきたら成功です。

** NG Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
Date: 日付
Hash: ハッシュ値
Time: 時間
chunk {inline} inline.bundle.js (inline) 3.85 kB [entry] [rendered]
chunk {main} main.bundle.js (main) 18 kB [initial] [rendered]
chunk {polyfills} polyfills.bundle.js (polyfills) 554 kB [initial] [rendered]
chunk {styles} styles.bundle.js (styles) 41.5 kB [initial] [rendered]
chunk {vendor} vendor.bundle.js (vendor) 7.43 MB [initial] [rendered]

webpack: Compiled successfully.

http://localhost:4200 を見てみましょう。
「Welcome to qiita!」と出てきたら成功ですね。

コンポーネントを作る

せっかくなのでブログ記事を作るタイプのコンポーネントを作ってみましょう。
Angularでは、普通にHTMLで「ページ」として定義するような単位のものも「コンポーネント」として定義します。

まずは@angular/cliからコンポーネントを生成してみます。

qiita_angular/のディレクトリ以下で、

ng generate component qiita

をやります。
src/app 以下に「qiita」というディレクトリが作成されていることがわかるかと思います。

qiita.component.htmlを見てみると以下の通りになっています。

<p>
  qiita works!
</p>

とりあえずこれでいってみましょう。
qiita.component.tsを見る限り、このコンポーネントは「app-qiita」というタグで埋め込めるようなので、
app.component.htmlを以下のようにしてみます。

<!--app.component.html-->
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>

<app-qiita></app-qiita>

この状態でlocalhost:4200 を見てみると、

Welcome to qiita!
qiita works!

と出るかと思います。

コンポーネントの中身を定義する

せっかくなのでtextareaとinputとbuttonでブログらしくしてみましょう。

ngModelは双方向バインディングで便利なので使います。
ngModelを使うためにはFormsModuleをapp.module.tsでインポートする必要があるので、思考停止してインポートしましょう。

// app.modules.ts
...
import { FormsModule } from '@angular/forms';
...

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

<!-- qiita.component.html -->
<p>
  <input type="text" [(ngModel)]="title">
</p>
<p>
  <textarea [(ngModel)]="body">
</p>
<p>
  <button (click)="onClickRegister()">登録</button>
</p>
// qiita.component.ts
export class QiitaComponent implements OnInit {

  title:string;
  body:string;

// ...

  onClickRegister() {
    console.log(this.title);
    console.log(this.body);
  }

}

こうするととりあえずブログ風になるかと思います。
見た目ダサいのは各自なんとかしましょう。

サービスを作る

Angularではビジネスロジック風の内容はサービス層に切り出します。
今回は登録とロードのロジックをサービス層に持たせます。

サービスの定義

まず@angular/cliからサービスを生成します。

ng generate service qiita/qiita

で、src/app/qiita以下にqiita.service.tsが生成されます。

とりあえず登録とロードのAPIだけ作っときましょう。
どうせ非同期なのでasyncで。

@Injectable()
export class QiitaService {

  constructor() { }

  public async load(id:number):Promise<any> {
    return new Promise((resolve)=>{
      resolve({title:'hoge',body:'fuga'});
    });
  }

  public async register(blog:any):Promise<any> {
    return new Promise((resolve)=>{
      resolve({title:blog.title,body:blog.title,id:1});
    });
  }

}

中身はまだ適当です。

コンポーネントに注入する

作ったサービスはコンポーネントにDIして使えるようにします。
@Injectableが付いたサービスクラスはコンポーネントのコンストラクタの引数に指定すると
自動でインスタンス化されて入ってきてくれます。
ただし@Componentの引数のprovidersにサービスクラスを指定しないとだめなので気を付けましょう。

いいですね。

// qiita.component.ts
import { Component, OnInit } from '@angular/core';
import { QiitaService } from './qiita.service'; // 足す

@Component({
  selector: 'app-qiita',
  templateUrl: './qiita.component.html',
  providers : [ QiitaService ], // 足す
  styleUrls: ['./qiita.component.css']
})
export class QiitaComponent implements OnInit {

  title:string;
  body:string;

  constructor(private service:QiitaService) { } // 足す

こんな感じにすると、いつでもthis.serviceから先程定義したサービスクラスのインスタンスが呼び出せます。

せっかくなので先程のonClickRegister()に追加しておきましょう。

  onClickRegister() {
    this.service.register({title:this.title,body:this.body}).then(res=>{
     console.log(res);
    });
  }

それっぽくなってきましたね。

Routerを整備する

今回はSPAを作りたいので、Routerを準備します。
SPAとはSingle Page Applicationの略で、ページ遷移時に同期通信を行わず、
JSでページを見た目上切り替えるようなアーキテクチャです。

ここでいう「ページ遷移」とは、

  • アドレスバーにアドレスを打ち込む
  • ブラウザの戻る、進むボタン

を指します。
要は、Angularがブラウザの戻る進むやアドレスバーを握るということです。すごいですね。

Routerを使い始める場合、以下のステップが必要です。

  • Routesを設定する。
  • app.module.tsにRouterModuleとRoutesをインポートする。
  • <router-outlet>を配置する

Routesを設定する。

ルーティングについては外だししておきます。
app.module.tsと同階層に「routes.ts」を作り、以下のようにします。

// routes.ts
import { Routes } from '@angular/router';
import { QiitaComponent } from './qiita/qiita.component';

export const routes : Routes = [
    {path : 'blog' , component : QiitaComponent }
];

app.module.tsにRouterModuleとRoutesをインポートする。

そのまんまです。

app.modules.tsに定義したroutesをインポートします。
最終的にこんな感じになります。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; // 足す。
import { AppComponent } from './app.component';
import { QiitaComponent } from './qiita/qiita.component';
import { FormsModule } from '@angular/forms';
import { routes } from './routes'; // 足した

@NgModule({
  declarations: [
    AppComponent,
    QiitaComponent,
  ],
  imports: [ 
     // ここから
    RouterModule.forRoot(
      routes,{useHash : true}
    ),
    //  ここまで足した
    BrowserModule,
    FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

router-outletを配置する

Routerを設定する場合、コンポーネントをどこに表示するのか教えてあげる必要があります。

今回はbootstrap:[AppComponent] としているため、AppComponent内にRouterでマッチしたコンポーネントを表示するイメージになります。

なので以下の通りにapp.component.htmlを書き換えます。

<!--app.component.html-->
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>

<router-outlet></router-outlet>

補足:useHashについて

useHashを使うとURLがhttp://localhost:4200/#/blog 的な感じになります。
こうすることにより、直接URLを叩いた際にはサーバー側としては「/」以下を見る挙動になるため、
ブラウザで描画する部分については常にサーバー側のルーティングではなくAngular側でのルーティングを行うことができます。
今回はSails.jsのルーターと共存するためuseHashを利用することとしました。

ここまでの動作確認

  • http://localhost:4200/#/blog を叩いて、テキストエリアとインプット、ボタンが表示されること。
  • welcome to qiita!と表示されること。

ここらへんを保証してあげたいですね。

このあたりでE2Eを書いておきましょう。

@angular/cliのe2eはデフォルトでprotractorというのを使っているようです。
使ったことないのですがとりあえずapp.e2e-spec.tsを参考にやってみましょう。

app.e2e

app.e2eについてはタイトルの文字が「app」であることをテストしているので、
今回は「qiita」に変えていることですし期待値を変えておきましょう。

//app.e2e-spec.ts
  it('should display welcome message', () => {
    page.navigateTo();
    expect(page.getParagraphText()).toEqual('Welcome to qiita!'); // ここを変えた
  });

qiita.e2eとqiita.po

app.e2e-spec.tsとapp.po.tsにならい、
qiita.e2e-spec.tsとqiita.po.tsを作っておきましょう。

poはPage Objectの略で、セレクタとかはこっちに書いておきます。

// qiita.po.ts

import { browser, by, element } from 'protractor';

export class QiitaPage {
  navigateTo() {
    return browser.get('/#/blog');
  }

  getInput() {
    return element(by.css('input'));
  }

  getTextArea() {
    return element(by.css('textarea'));
  }

  getButtonText() {
    return element(by.css('button')).getText();
  }
}

いい感じですね。

// qiita.e2e-spec.ts
import { QiitaPage } from './qiita.po';

describe('qiita App', () => {
  let page: QiitaPage;

  beforeEach(() => {
    page = new QiitaPage();
  });

  it('should display textarea and input and button', () => {
    page.navigateTo();
    expect(page.getInput()).toBeTruthy();
    expect(page.getTextArea()).toBeTruthy();
    expect(page.getButtonText()).toEqual('登録');
  });
});

これで少なくとも、input要素とtextarea要素と「登録」という名称のbutton要素が
あるということは保証できました。
いわゆるスモークテストというやつですが、初期の環境構築の段階では十分かと思われます。

yarimasitane.

さらにRouter

せっかくなので登録したブログが見れるようにしましょう。

routes.tsにルーティングを追加します。

{path : 'blog/:id' , component : QiitaComponent }

idというパラメータに一致するブログ記事をロードしてきて見せる感じになりそうです。
いいですね。

せっかくなのでngOnInitにロード処理を書いておきましょう。

// qiita.component.ts
import { ActivatedRoute } from '@angular/router'; // 追加

...

  constructor(private service:QiitaService,private route : ActivatedRoute) { } // ActivatedRouteを追加。

  ngOnInit() { // 書き換え。
    this.route.params.subscribe(params=>{
      let id = params['id'];
      if(!id) {
        return;
      }
      this.service.load(id).then(blog=>{
        this.title = blog.title;
        this.body = blog.body;
      });
    });
  }

ルーターからパラメータをもらってきて、それでロードする感じになりました。
かなりいい感じですね。

せっかくなのでさらに登録後に照会画面に遷移するようにしておきましょう。

// qiita.component.ts
  constructor(private service:QiitaService,private route : ActivatedRoute,private router : Router) { } // routerを追加
...
  onClickRegister() {
      this.service.register({title:this.title,body:this.body}).then(res=>{
       this.router.navigate(['blog/' + res.id]); // 遷移するように。
      });
  }

さて、コンポーネントをいろいろいじくったので、このあたりでe2eを回しときましょう。

ng e2e

でe2eテストを実行できます。

ロード時にも表示できることをテストしておきたいので、PageObjectをこんな感じにしてみます。

// qiita.po.ts
export class QiitaPage {
  navigateTo(id?:number) {
    return browser.get('/#/blog' + (id ? '/' + String(id) : ''));
  }

そのうえでテストコード。

// qiita.e2e-spec.ts
...
  it('should display textarea and input and button even when an id specified.', () => {
    page.navigateTo(1);
    expect(page.getInput()).toBeTruthy();
    expect(page.getTextArea()).toBeTruthy();
    expect(page.getButtonText()).toEqual('登録');
  });
});

再度回してみます。

ng e2e

いいですね~

とりあえずAngularの項は以上です。

Sailsをはじめる

sails new

とりあえずSails用のディレクトリを作っときます。qiita_sailsとしておきます。
qiita_sails以下で

sails new

してみましょう。

 1. Web App  ·  Extensible project with auth, login, & password recovery
 2. Empty    ·  An empty Sails app, yours to configure
 (type "?" for help, or <CTRL+C> to cancel)
?

とりあえず面倒なので2.でいきます。

sailsのディレクトリ構成

qiita_sails
├─api
│  ├─controllers
│  ├─helpers
│  ├─models
│  └─policies
├─assets
│  ├─dependencies
│  ├─images
│  ├─js
│  ├─styles
│  └─templates
├─config
│  ├─env
│  └─locales
├─tasks
│  ├─config
│  └─register
└─views
    ├─layouts
    └─pages

こういう感じになってます。
基本的には「api」以下が手を入れる箇所になります。
ただし「config」以下も見ておくとかなり勉強になるのでひととおり見ておきましょう。

sails.config

めぼしいところを紹介していきます。

datastores.js (旧 connection.js)

adapterと呼ばれるプラグイン群を利用して、いろいろなDBに接続することができます。

https://sailsjs.com/plugins/databases
によれば、公式でサポートされているのはMySQL、PostgreSQL、MongoDBのようです。
デフォルトではsails-diskという組み込まれているDBを利用する設定となっています。
sails-diskのDBの中身は.tmp/localDiskDb 以下にJSON形式で保存されてるので見れます。

とりあえず今回は特に設定しませんが、
ここで設定することが可能です。

security.js (旧 cors.js , csrf.js )

セキュリティ設定をすることができます。

今回は既にAngular側でAPIを叩く準備ができているため、
CSRF防護はのちのちONにしておきましょう。

今回はpostman(またはcurl)から動作確認がしたいので、いったんOFFったままにしておきます。

models.js

sailsはrailsと同様にORマッパーによるDB操作を行います。
Modelとして定義したデータモデルとDBのテーブル(やコレクション)が一致するように、
環境のマイグレーションを自動で行ってくれます。

それをどうするかを設定する必要がありますが、
いったん「alter」としておきます。(モデルに変更があったときは、テーブルをalterすることで対応するストラテジー)

他には一度テーブルをドロップする「drop」と、何もしない「safe」があります。

  /***************************************************************************
  *                                                                          *
  * How and whether Sails will attempt to automatically rebuild the          *
  * tables/collections/etc. in your schema.                                  *
  *                                                                          *
  * > Note that, when running in a production environment, this will be      *
  * > automatically set to `migrate: 'safe'`, no matter what you configure   *
  * > here.  This is a failsafe to prevent Sails from accidentally running   *
  * > auto-migrations on your production database.                           *
  * >                                                                        *
  * > For more info, see:                                                    *
  * > https://sailsjs.com/docs/concepts/orm/model-settings#?migrate          *
  *                                                                          *
  ***************************************************************************/

  migrate: 'alter',

APIを作る

今回はBlogというモデルを作り、それに対してCR(UD)ができるようにします。
(Update , Deleteについては本稿では割愛します。)

sailsはrailsと同様にコマンドラインからモデルやコントローラの作成が自動で行えます。

sails generate api blog

とすると、api/controllers 以下にBlogController.jsが、api/models 以下にBlog.jsが生成されます。

sails.jsはBlueprintという機能により、モデルとコントローラを作った時点でCRUDのREST APIが自動で作られるようになっています。

ということで、Sails側でやることはいったんここで終わりです。

サーバーを立ち上げる

sails lift

です。

http://localhost:1337 で起動します。
とりあえずアクセスしてなんか出てくるのを確認しましょう。

APIを叩いてみる

今回はchrome拡張の「Postman」を利用します。
curlでもいいので適宜読み替えてください。

POST

とりあえずcreateをやってみます。

メソッドを「POST」にし、
http://localhost:1337/blog
に対し、
「Body」タブから「x-www-form-urlencoded」を選択し、

title : hoge
body : fuga

みたいなのを打って「Send」からPOSTしてみます。

GET

成功すると作成したリソースの内容が返ってくると思うので、
その「id」を控えておき、
以下のGETを叩いてみましょう。

メソッドを「GET」にし、Bodyを送らずに、
http://localhost:1337/blog/さっきのID

先程作成したリソースが返ってくると思います。

http://localhost:1337/blog
だったら一覧が配列で返ってきます。

いいですね。

PUT

割愛します。

DELETE

割愛します。

Sails.js と Angular5 を統合する

本題です。
やることとしては、

  • Angular側はビルドだけしておき、成果物を吐く。
  • Sails側は成果物をアセットパイプラインで受け取り、サーバー上に配置する。
  • Angular側がビルドした結果できるindex.htmlを、Sails側のテンプレートエンジンでインクルードして表示する。

的な感じです。
順を追ってやっていきます。

Angularのプロジェクトをassets以下に配置する

qiita_sails/以下にあるassets配下にディレクトリを作っておきます。
名前はangularとすることにしました。

次にangular以下に先程Angularのプロジェクトとして作成した方のディレクトリから、
src以下を移植してきます。

オラッ

qiita_sails
├─api
│  ├─controllers
│  ├─helpers
│  ├─models
│  └─policies
├─assets
│  ├─angular
│  │  ├─app
│  │  │  └─qiita
│  │  ├─assets
│  │  └─environments
│  ├─dependencies
│  ├─images
│  ├─js
│  ├─styles
│  └─templates
├─config
│  ├─env
│  └─locales
├─tasks
│  ├─config
│  └─register
└─views
    ├─layouts
    └─pages

こんな感じになるはずです

@angular/cli関連の設定を移植する

次に.angular-cli.json をpackage.jsonのあるディレクトリにコピーします。
続いてtsconfig.jsonをassets/以下にコピーします。

続いて、.angular-cli.jsonは中に相対パスを持ってるので、
それを修正してあげます。

まずは"apps" 以下の "root" です。

こんな感じに書き換えときます。

"apps": [
    {
      "root": "assets/angular",
      "outDir": "assets/dist",

"lint" も同様にしときましょう。
ただしe2eだけは後で違う場所に置くので、いったん以下のようにしておきます。

  "lint": [
    {
      "project": "assets/angular/tsconfig.app.json",
      "exclude": "**/node_modules/**"
    },
    {
      "project": "assets/angular/tsconfig.spec.json",
      "exclude": "**/node_modules/**"
    },
    {
      "project": "e2e/tsconfig.e2e.json",
      "exclude": "**/node_modules/**"
    }
  ],

package.jsonの依存関係を移植する

元のAngularのプロジェクトの方のpacakge.jsonから、sailsの方のpackage.jsonにdependencyを移植してきます。
ここはノリで大丈夫です。

参考までに筆者の構成です

  "dependencies": {
    "sails": "^1.0.0",
    "grunt": "1.0.1",
    "sails-hook-grunt": "^3.0.2",
    "sails-hook-orm": "^2.0.0-16",
    "sails-hook-sockets": "^1.4.0",
    "@sailshq/connect-redis": "^3.2.1",
    "@sailshq/socket.io-redis": "^5.2.0",
    "@sailshq/lodash": "^3.10.3",
    "async": "2.0.1",
    "@angular/animations": "^5.2.0",
    "@angular/common": "^5.2.0",
    "@angular/compiler": "^5.2.0",
    "@angular/core": "^5.2.0",
    "@angular/forms": "^5.2.0",
    "@angular/http": "^5.2.0",
    "@angular/platform-browser": "^5.2.0",
    "@angular/platform-browser-dynamic": "^5.2.0",
    "@angular/router": "^5.2.0",
    "core-js": "^2.4.1",
    "rxjs": "^5.5.6",
    "zone.js": "^0.8.19"
  },
  "devDependencies": {
    "@sailshq/eslint": "^4.19.3",
    "@angular/cli": "~1.7.4",
    "@angular/compiler-cli": "^5.2.0",
    "@angular/language-service": "^5.2.0",
    "@types/jasmine": "~2.8.3",
    "@types/jasminewd2": "~2.0.2",
    "@types/node": "~6.0.60",
    "codelyzer": "^4.0.1",
    "jasmine-core": "~2.8.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "~2.0.0",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "^1.2.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "protractor": "~5.1.2",
    "ts-node": "~4.1.0",
    "tslint": "~5.9.1",
    "typescript": "~2.5.3"
  },

npm scriptsでビルドを仕込む

yarn startしたときにsails lift もng build もしてほしいので、
パラレルで走るようにします。

今回はnpm-run-allを利用します。

$> yarn add npm-run-all -D

これを利用してyarn startしたときにビルドとサーバーの起動が同時に行われるようにします。

"scripts" : {
...
    "start:dev": "npm-run-all --parallel lift:dev build:dev",
    "build:dev": "ng build --watch --output-path=./assets/dist --base-href=/ --deploy-url=/dist/",
    "lift:dev": "sails lift",
    "inspect:dev" : "sails inspect",
...
}

こういう感じにしておきます。

補足:build:devのオプションについて

ng buildの引数に渡しているものについてですが、

--output-pathは単純にビルドした成果物をどこに吐くかを定義します。

--base-hrefはRouterがルートとして認識するパスをどこにするかを定義します。
この場合は、AngularのRouterから見てルートパスを実際のURL上の"/"とする、
つまり名実ともにルートとする設定にしています。

--deploy-urlは実際にデプロイするものがどこにあるのかを定義します。
ここで「/dist/」としている意図としては、Angularのアプリから見たアセット(main.bundle.jsやコンポーネントごとに定義したcssなど)は、
実際にはSailsのサーバー上のアセットが置かれるディレクトリに直に置かれるわけではなく、
あくまでAngular側のアプリをビルドした成果物が配置されるディレクトリ(つまり、Sailsのアセットフォルダのサブフォルダassets/dist)に配置されるということを明示するというものです。
これをやらないと、Angular側が要求するアセットは全て存在しないことになってしまい、画面がちゃんと開きません。

Angular側のindex.htmlをSails側のテンプレートエンジンでインクルードする

とりあえずqiita_sails/views/pages以下のhomepage.ejsを以下の内容で置き換えます。

<!DOCTYPE html>
<html>

<!-- webpack generate html -->
<%- partial('../../assets/dist/index.html') %>

</html>

以上です。

次は画面を動かしてみましょう。

画面を確認!

$> yarn start:dev

http://localhost:1337/#/blog

を見てみるとしましょう。

当然E2Eも移植する

qiita_angular/e2eを、qiita_sails/e2eに移植します。
あと、protractor.conf.jsもqiita_sails以下に置いておきましょう。
コンパイル用に、assets/以下に置いたtsconfig.jsonもqiita_sails以下にコピーしておきます。

protractor.conf.jsは、baseUrlだけ変えておきます。

baseUrl: 'http://localhost:1337/',

この状態で

ng e2e

すると、前回同様にテストが成功することがわかると思います。

Angular側のサービスとSails側のAPIを繋ぐ

さあ、残すはAPIとの接続のみとなりました。
やっていきましょう。

assets/angular/app/qiita (深いな・・・)のqiita.service.tsをやっていきます。

HttpClientをDIする

とりあえずHttpClientをDIしてきます。

import { HttpClient } from '@angular/common/http';

@Injectable()
export class QiitaService {

  constructor(private http:HttpClient) { }

HttpClientを含むモジュールをapp.module.tsでインポートします。

...
import { HttpClientModule } from '@angular/common/http';


@NgModule({
...
  imports: [
...
    HttpClientModule, // 足す。
...
  ],
  providers: [  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

とりあえず、この状態で

ng e2e

いいですね。

リクエストをする

もともと空のresolveを書いていたPromiseを書き換えるだけです。

  public async load(id:number):Promise<any> {
    return new Promise((resolve,reject) => {
      this.http.get('/blog/' + String(id)).subscribe(
      data => resolve(data),
      error => reject(error)
      );
    });
  }

  public async register(blog:any):Promise<any> {
    return new Promise((resolve,reject)=>{
      this.http.post('/blog',blog).subscribe(
        data => resolve(data),
        error => reject(error)
        );
    });
  }

よく考えたら最初からObservableを返す作りでもよかった。。。

とりあえずこの状態で

ng e2e

しときますか。

とりあえず、以上です

まとめ

  • @angular/cliでコンポーネントを作成すると便利。
  • Sails側のアセットフォルダにAngularのソースを入れとく。
  • Sailsのサーバーが起動するときにAngular側のビルドを並行して走らせる。
  • Angularのindex.htmlをSailsのテンプレートエンジンでインクルードすれば万事OK!
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.