Ionic(v5) でスマホから kintone 連携 (前編)

はじめまして、アールスリーの浅利と申します! よろしくお願いします!!

最近(お仕事で) Ionic Framework というものを使ってスマホアプリを作ったのですが、その Ionic についての紹介をしてほしいと頼まれましたので、 前編・後編に分けて、簡単なスマホアプリを1つ作りながら、Ionic Framework の紹介をしたいと思います。

Ionic Framework とは?

Ionic Framework は、Webアプリ開発の応用で、スマホ向けのリッチな見た目のアプリを作れるというフレームワークです。

https://ionicframework.com/

ブラウザ向けのWebアプリも作れますが、(前編では紹介しませんが)スマホネイティブアプリも作成できます。後編で紹介する予定です!

Webアプリの開発の経験があり、Angular または React の知識がある人であれば、 Ionic Framework を使ってスマホ向けのアプリが作れます。

今回は、現時点で最新の Ionic Framework v5 を使って、(Serverless経由で) kintoneのレコードを表示するアプリを作ってみます。

まずはテンプレートから作成

Ionic のアプリを開発するためには、

  • node、npm、(npx)
  • Visual Studio Code 等の開発環境
  • Ionic CLI

の準備が必要です。

また、今回は

  • Serverless

も使用します。

node、npm や開発環境の準備についてはここでは省略します。(npx は 最新の npm には付いてきます)

Ionic CLI のローカルインストール

Ionic CLI は、インストールすることで ionicコマンドが実行できるようになります。 ionicコマンドを使用して、アプリをひな形から作成したり、ページを追加したり、ビルドしたりということをします。

通常は Ionic CLI をグローバルインストールするようインストールガイドには書かれていますが、 Ionic はバージョンが変わっていくため、 複数のバージョンで作ったアプリがあってメンテしないといけない、というような場合には問題となってしまいます。 (グローバルを汚してしまう)

そのため、今回は グローバルインストールせずに済む方法を紹介します。

インストールガイドでは、Ionic CLI をグローバルインストールした後にアプリをひな形から作成、という流れなのですが、 今回は、npx を使って ionic コマンドをインストールせずに実行し、アプリをひな形から作成します。 作成した後に、出来たフォルダへ Ionic CLI をローカルインストールします。

以下のように、npx コマンドを使用することで、 (ionicをインストールせずに、) ionic コマンドを実行できます。 引数の意味は -p パッケージ名で ionicのパッケージの指定で、そのパッケージ内の ionic コマンドを実行します。

$ npx -p @ionic/cli ionic start ionicTest tabs

ionic の後ろが ionicコマンドへの引数で、start アプリ名 ひな形名 となります。 これにより、タブ付きの ionicTest というアプリが作成されます。

途中、JavaScript のフレームワークをAngular/Reactどちらにするか聞かれますが、今回は Angular を選択しました。 ( 以降、Angular で実装していきますが、React の方が得意な方はここで React を選択し、以降は読み替えてください。 )

コマンド実行が終わると、アプリ名のフォルダ(ionicTest)ができるので、その中を開いてください。 その中で改めて Ionic CLI をローカルインストールします。

$ cd ionicTest
$ npm install --save-dev @ionic/cli

ローカルインストールしたので、このフォルダ内で npx ionic 引数 とコマンドを実行すると、ローカルインストールされた ionic が実行されます。 これでグローバルを汚しません。

最初の実行

試しに、現在の状態で実行してみましょう。

$ npx ionic serve

作成直後のアプリ

Webサーバーが起動し、http://localhost:8100/ というURL上でIonicアプリが起動されます。(自動的にブラウザが立ち上がると思います。) 下のタブをクリックすると、Tab1/Tab2/Tab3を切り替えできます。

なお、この状態でファイルを修正すると、自動的にビルドが走り、ブラウザで表示している画面が更新されます。 このように、PC上ですぐに動かしながらアプリを開発していくことが出来ます。 この画面をスマホから表示させれば、最終的にスマホアプリになった時の状態の雰囲気もつかめるかと思います。 (ただし、ブラウザで動かす場合と、スマホアプリにして動かす場合とでは、スマホネイティブにしかない機能が動かない等の制約はあります。)

Ctrl+C でサーバーを終了できますので、終了させておいてください。

kintone側のアプリの用意

前編・後編で、スマホのGPSの緯度・経度をkintoneへ登録するようなアプリを作りたいと思います。 (今回はkintoneアプリに登録済みのデータの一覧表示まで。緯度・経度の登録は後編の予定です。)

kintoneへ、「ロケーション」という名前のアプリを用意し、 緯度・経度を一行文字列フィールドとして追加しました。 また、名前フィールドも用意しています。

フィールドコードフィールド名タイプ
name名前文字列(1行)
latitude緯度文字列(1行)
longitude経度文字列(1行)
ロケーションアプリのフォーム

さらに、このアプリを kintone REST API を使って閲覧/登録したいので、APIトークンを生成しました。 生成したAPIトークンの文字列は後で使用します。

APIトークン

Serverless Framework でローカルAPIサーバー

実は、Ionic アプリで、JavaScript(TypeScript)用のライブラリを使用して kintone REST API を実行しようとすると、 クロスサイトリクエストとなってしまい、エラーになってしまいます。

スマホネイティブ実装のHTTPクライアントプラグイン を使うと回避できるようですが、 今回はそれを使いません。

今回は、Serverless Framework のオフラインモードを使って、自分のPCをAPIサーバー化し、 そこへ Ionic アプリからアクセスすると、APIサーバー側で kintone API が実行されるようにしようと思います。 この部分に関しては今回は詳しい説明はせずに、サクッと作ってしまいます。

新しいフォルダを適当な名前で作成し、その中で以下を実行します。

$ npx serverless create -t aws-nodejs -n わかりやすい名前

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

$ npm init
$ npm install --save-dev serverless
$ npm install --save aws-sdk
$ npm install --save @kintone/rest-api-client

serverless をオフライン(ローカル)実行するためのプラグインをインストールします。

$ npx sls plugin install -n serverless-offline

handler.js を以下のようにして、 全件取得・1件取得・新規登録・更新のAPIを実装しました。SUBDOMAIN、APP_ID、API_TOKEN の値は、先ほど用意した kintoneアプリ「ロケーション」の環境にあわせて書き換えてください。

'use strict';

const { KintoneRestAPIClient } = require("@kintone/rest-api-client");

// (実際は、環境変数等を使うべき)
const SUBDOMAIN = あなたのkintone環境のサブドメイン(.cybozu.comの前の部分);
const APP_ID = kintoneアプリのID;
const API_TOKEN = APIトークン;


const client = new KintoneRestAPIClient({
  baseUrl: `https://${SUBDOMAIN}.cybozu.com`,
  auth: {
    apiToken: API_TOKEN
  }
});


const createResponse = (statusCode, data) => {
  return {
    statusCode,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Credentials": true,
      "Cache-Control": "no-cache"
    },
    body: JSON.stringify(data, null, 2),
  };
}

const handler = async (func) => {
  try {
    const data = await func();
    return createResponse(200, data);
  } catch (error) {
    console.log(error);
    return createResponse(500, { error: error.message });
  }
}


module.exports.getAllRecords = async () => {
  // 全レコードを取得
  return handler(async () => {
    const records = await client.record.getAllRecords({
      app: APP_ID
    });
    return { records };
  });
};

module.exports.getRecord = async (event) => {
  // 指定したレコードIDの一件を取得
  return handler(async () => {
    const recordId = event.queryStringParameters.id;
    const record = await client.record.getRecord( {
      app: APP_ID,
      id: recordId
    });
    return { record };
  })
};

module.exports.addRecord = async (event) => {
  // データ登録
  return handler(async () => {
    const body = event.body;
    const data = JSON.parse(body);

    const record = {
      name: { value: data.name },
      latitude: { value: data.latitude },
      longitude: { value: data.longitude },
    };

    return await client.record.addRecord({
      app: APP_ID,
      record
    });
  });
};

module.exports.updateRecord = async (event) => {
  // id, revision のデータを更新
  return handler(async () => {
    const body = event.body;
    const data = JSON.parse(body);

    const recordId = data.id;
    const revision = data.revision;
    const record = {
      name: { value: data.name },
      latitude: { value: data.latitude },
      longitude: { value: data.longitude },
    };

    return await client.record.updateRecord({
      app: APP_ID,
      id: recordId,
      revision,
      record
    });
  });
};

serverless.yml は以下のようにします。 設定したのは functions: の中身だけです。

service: あなたが「serverless create」で付けた名前

provider:
  name: aws
  runtime: nodejs12.x

functions:
  get_all_records:
    handler: handler.getAllRecords
    events:
      - http:
          path: records
          method: get
          cors: true

  get_record:
    handler: handler.getRecord
    events:
      - http:
          path: record
          method: get
          cors: true

  add_record:
    handler: handler.addRecord
    events:
      - http:
          path: addRecord
          method: post
          cors: true

  update_record:
    handler: handler.updateRecord
    events:
      - http:
          path: updateRecord
          method: post
          cors: true

plugins:
  - serverless-offline

これで、 以下のコマンドを実行すると、PCがAPIサーバーになります。

$ npx sls offline

http://localhost:3000/records というURLをブラウザで開くと、kintoneアプリ「ロケーション」に入れたデータが JSON形式で取得できるか、確認してください。( 緯度経度は仮の値でデータを用意してください)

表示できましたか? (表示できない場合は、SUBDOMAIN、APP_ID、API_TOKENの設定を見直してください。)

今回は同じネットワーク上で Ionic アプリを動かすために、サクッとAPIを作成しましたが、実環境では、AWS環境上で動かすようにすればいいと思います。ただし、セキュリティ等は必ず見直してくださいね!

Ionic側アプリの作成

Ionicに戻って、 先ほどひな形から作成したアプリに、手を加えていきます。

ひな形で、Tab1,2,3 の三画面構成のアプリがすでに用意されてますので、

  • Tab1 : 一覧画面
  • Tab2 : 新規登録/編集画面
  • Tab3 : 設定画面

のようにしてしまいましょう。

必要なライブラリのインストール

まず、npmでライブラリをインストールします。 前編ではとりあえず ↓これだけです。

APIサーバーにアクセスするための Axiosライブラリ

https://github.com/axios/axios

$ npm install --save axios

画面の改造

画面は、今ある Tab1, Tab2, Tab3 を改造していきます。 まず以下のコマンドで起動してみます。画面は、先ほどと同じ画面が表示されます。

$ npx ionic serve

1. 下のタブバーの設定

タブバーは、/src/app/tabs/tabs.page.html に書かれています。 ラベルを「一覧」「登録」「設定」にします。アイコンも変えてみました。

/src/app/tabs/tabs.page.html

    ...
    <ion-tab-button tab="tab1">
      <ion-icon name="list-outline"></ion-icon>
      <ion-label>一覧</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="tab2">
      <ion-icon name="add-circle"></ion-icon>
      <ion-label>登録</ion-label>
    </ion-tab-button>

    <ion-tab-button tab="tab3">
      <ion-icon name="settings-sharp"></ion-icon>
      <ion-label>設定</ion-label>
    </ion-tab-button>
    ...

2. 設定画面の用意

次に、設定画面(Tab3)を実装し、

  • APIサーバーのURL

を登録できるようにします。

Tab3のコードは、/src/app/tab3/ にあります。 この設定画面で、APIサーバーのURLを登録できるようにするため、メンバ変数 apiUrl を追加しました。

/src/app/tab3/tab3.page.ts

...
export class Tab3Page {

  private apiUrl: string;    ← apiUrl を追加

画面のテンプレート tab3.page.html を編集し、APIサーバーのURL用の入力エリアと、登録ボタンを用意します。 <ion-input>が入力エリア、<ion-button>がボタンです。

/src/app/tab3/tab3.page.html

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      設定画面            ← Tab3 だったのを変更
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">設定画面</ion-title>     ← Tab3 だったのを変更
    </ion-toolbar>
  </ion-header>

  ↓ <app-explore-container>~は削除して、以下追加

  <ion-list>
    <ion-item>
      <ion-label>APIサーバーURL</ion-label>
      <ion-input [(ngModel)]="apiUrl"></ion-input>
    </ion-item>
  </ion-list>

  <ion-button expand="block" (click)="register()">登録</ion-button>
  ↑ ここまで
</ion-content>

ファイルを保存すると、画面が自動的に変わります。

設定画面

これで見た目だけは出来ました。次は中身の実装をしなければなりません。

まず、初期化時には、現在保存されている値を表示するようにしましょう。 また、登録ボタンの処理(register())を用意していませんので、実装します。 登録したら「登録完了」とメッセージが表示されるようにします。

また、ngOnInit()にて、この画面の初期化時に、前回登録した値を表示するようにしています。

/src/app/tab3/tab3.page.ts

import { Component, OnInit } from '@angular/core';   ← OnInit 追加
import { AlertController } from '@ionic/angular';   ← この行 追加
...
export class Tab3Page implements OnInit {    ← implements OnInit を追加

  constructor(private alertController: AlertController) {}    ← constructor()の引数に追加

  ↓ 以下追加

  /** 画面初期化イベント */
  ngOnInit() {
    try {
      this.apiUrl = localStorage.getItem('apiUrl');
    } catch (err)  {
      console.error(err);
    }
  }

  /** 登録ボタンクリック */
  async register() {
    try {
      localStorage.setItem('apiUrl', this.apiUrl);

      // メッセージダイアログの表示
      const alert = await this.alertController.create({
        message: '登録完了',
        buttons: ['OK']
      });

      await alert.present();

    } catch (err)  {
      console.error(err);
    }
  }

設定画面

3. kintone サービスを作成

kintone を操作するために、KintoneService というサービスを作りたいと思います。 Angular なら ng generate service ~ のようなことをしますが、ionicでは以下のようにします。

$ npx ionic generate service kintone

すると、/src/app/ の下に kintone.service.ts が作成されます。

KintoneService の実装の前に、AppModule にて設定をしてしまいましょう。 KintoneService を import して、providers に追加します。 こうすることで、画面等へインジェクトされるようになります。

/src/app/app.module.ts

...
import { KintoneService } from 'src/app/kintone.service';       ← 追加
...
@NgModule({
  ...
  providers: [
    ...
    KintoneService,  ← 追加

そして KintoneService 側を実装します。 Tab3で設定する apiUrl をこちらにも持ち、 ready() で(もし初期化されていなければ) localStorageから取得し、 setParams() でlocalStorageへ保存をするようにします。

/src/app/kintone.service.ts

...
export class KintoneService {

  private initalized: boolean;
  public apiUrl: string;

  constructor() {}

  async ready(): Promise {
    if (!this.initalized) {
      this.apiUrl = localStorage.getItem('apiUrl');
      this.initalized = true;
    }
  }

  async setParams(apiUrl: string): Promise {
    this.apiUrl = apiUrl;
    localStorage.setItem('apiUrl', apiUrl);
  }
}

Tab3から、KintoneService を使用できるようにしましょう。 そして、Tab3から直接localStorageへ取得/登録をするのではなく、KintoneServiceを使うように変更しましょう。

先ほど AppModule で設定しましたので、Tab3 の constructor() に指定するだけで this.kintone.~~ という形で使えるようになります。

/src/app/tab3/tab3.page.ts

import { KintoneService } from 'src/app/kintone.service';  ← 追加
...

  constructor(private alertController: AlertController,
              private kintone: KintoneService) {}   ← constructor()の引数に追加


  async ngOnInit() {   ← async 追加しました
    try {
      ↓ localStorage.getItem() ではなく、KintoneService の ready()を待ってから、apiUrl を取り出すようにしました。
      await this.kintone.ready();
      this.apiUrl = this.kintone.apiUrl;
    } catch (err) {
      ...

  async register() {
    try {
      await this.kintone.setParams(this.apiUrl);   ← localStorage.setItem() ではなく、kintone.setParams() に変更しました

      const alert = ...

4. データを取得して一覧画面に表示する

先ほど作成した KintoneService に、APIサーバー経由で、kintoneアプリのレコード一覧を取得できるメソッドを追加しましょう。

/src/app/kintone.service.ts

...
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; ← 追加
...
export class KintoneService {
  ...
  private initalized: boolean;
  public apiUrl: string;
  private client: AxiosInstance;   ← 追加
  ...
  ↓ 追加
  private createClient(baseURL: string): AxiosInstance {
    return axios.create({
      baseURL,
      headers: {
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest'
      },
      responseType: 'json'
    });
  }
  ↑ ここまで
  ...
  async ready(): Promise {
    if (!this.initalized) {
      this.apiUrl = localStorage.getItem('apiUrl');
      this.client = this.createClient(this.apiUrl);    ← 追加
  ...
  async setParams(apiUrl: string): Promise {
    this.apiUrl = apiUrl;
    this.client = this.createClient(this.apiUrl);    ← 追加
  ...

  ↓ 以下追加

  async getAllRecords(): Promise<[]> {
    const data = await this.request('GET', 'records');
    return data.records;
  }

  private async request(method: 'POST'|'GET', path: string, data: {} = {}): Promise {
    const conf: AxiosRequestConfig = {
      url: path,
      method
    };
    if (method === 'POST') {
      conf.data = data;
    } else {
      conf.params = data;
    }
    const response = await this.client.request(conf);
    if (response.status === 200) {
      return response.data;
    } else {
      console.error(response);
      throw Error('request 失敗');
    }
  }
  ↑ ここまで

続いて、取得したデータを Tab1 画面に表示するようにします。

/src/app/tab1/tab1.page.html

<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      一覧             ← 修正
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">一覧</ion-title>       ← 修正
    </ion-toolbar>
  </ion-header>

  ↓ <app-explore-container>~は削除して、以下追加

  <ion-spinner *ngIf="loading"></ion-spinner>
  <ion-list *ngIf="records">
    <ion-item *ngFor="let r of records">
      <ion-label>
        <h3>{{ r.name.value }}</h3>
        <p>緯度:{{ r.latitude.value }} , 経度:{{ r.longitude.value }}</p>
      </ion-label>
    </ion-item>
  </ion-list>
  ↑ ここまで
</ion-content>

/src/app/tab1/tab1.page.ts

import { KintoneService } from 'src/app/kintone.service';   ← 追加
...
export class Tab1Page {

  loading: boolean;   ← 追加
  records: object[];   ← 追加

  constructor(private kintone: KintoneService) {}    ← constructor()の引数に追加

  ↓ 以下追加
  async ionViewWillEnter() {
    try {
      this.loading = true;
      this.records = null;
      await this.kintone.ready();
      this.records = await this.kintone.getAllRecords();
    } catch (err) {
      console.error(err);
    } finally {
      this.loading = false;
    }
  }
}

これで、kintoneアプリ側でデータを投入した後、一覧画面(Tab1)を表示すると、 そのデータが表示されます!

一覧画面の動作テスト

5.スマホのブラウザで表示してみる

これまで PC上で表示させていましたが、同じネットワークにつないだスマホのブラウザで、表示することもできます。

そのためにはまず、ionic serve と sls offline について、引数で、自分のPC以外からの接続を許可するよう設定する必要があります。 それぞれ一度終了させた後、以下のようにしてください。

Ionic側

$ npx ionic serve --external

Serverless側

$ npx sls offline --host 0.0.0.0

また、必要があれば、ファイアウォールの設定でそれぞれのポート(8100と3000)を開けてあげてください。

この状態で、同じネットワークにスマホをWiFi接続し、 (PCのIPアドレス):8100 をブラウザで開くと、同じ画面が表示されます。

設定画面にて、http://(PCのIPアドレス):3000 を登録すれば、一覧画面にデータも表示されます。

スマホで表示

…後編へ続く

前編は以上です。

まだ Ionic Framework の導入くらいで、 スマホっぽいことができてないですが、 Ionic Framework で簡単にWebアプリが作成できることは理解していただけると思います。

今回紹介したのは Ionic Framework のほんの一部で、UIもリストとボタンとダイアログくらいしか使っていませんが、ほかにもいろいろ試していただけたらなと思います!

後編では、このアプリの続きで、「GPS使ってデータ登録」「スマホネイティブアプリ化」(Android) をやりたいと思います。

投稿者プロフィール

アバター画像
淺利(あさり)
Webとか.NETとかスマホとかマイコンとかシーケンサーとか、いろいろやってるエンジニアです