IonicとCapacitorを使ったモバイルアプリをCLIによる初期ファイル生成なしで構築する

ウェブの技術を使ってAndroidやiOSなどのモバイルアプリを作成できればコードの共通化ができ、OSごとに個別のアプリ開発の知識を要求されることがなくなります。このようなことを実現するためのフレームワークに[Ionic](https://ionicframework.com)と[Capacitor](https://capacitorjs.com/)があります。

本稿ではIonicとCapacitorとVueを使ったモバイルアプリをIonicの公式ドキュメントにあるCLIによる初期ファイル生成を使わずに一から作成することで、構成を深く理解することを目指します。特にIonicとCapacitorの関係性は明確な説明が無いせいでわかりづらいのですが、そこを理解し実際に最低限のアプリを作成することで確認します。

作成したコードは以下から見られます。コードを早くみたい人はどうぞ。

https://github.com/silane/capacitor-ionic-vue-sample

# Scaffold（CLIによる初期ファイル生成）すると中身が理解できない
Ionicでのモバイルアプリ作成の話はどうもその中身が理解しづらいです。Ionicの公式ドキュメントではCLIを使って初期ファイルを生成する方法（scaffolding）でセットアップする方法を書いており、また、Ionicの話とCapacitorの話が混じっています。Scaffoldingによって最初から大量のファイルが生成され、どのライブラリがどのファイルを使っているのか分かりづらい上、ドキュメント上でもIonicとCapacitorの分担範囲について明確に書いていない気がします。とりあえず動けばなんでもいいというスタンスならscaffoldingでできたファイルをいじっていればそれでいいのでしょうが、エンジニアならscaffoldingでできたファイルがそれぞれ何をしているのか、どんなライブラリで構成されていてそれぞれがどのように関係しあっているのか、中身をちゃんと理解したものを使いたいです。そうでないとドキュメントに書かれたことしかできず、少し外れたことがしたいと思ってもやり方がわかりません。

ということで、IonicとCapacitorによるモバイルアプリをscaffoldingなしで自力でセットアップしてみる、というのがこの記事です。

# Scaffoldingで使われるライブラリ
まずはドキュメント通りにscaffoldingでコードを生成した場合にどんなライブラリが使われているのかを調べます。私はVueが好きなのでVueでセットアップした場合です。以下にめぼしいもを挙げます。実際には連携のためのプラグイン的パッケージ等、他にもたくさん入っています。

- dependencies
    - @capacitor/app
    - @capacitor/core
    - @capacitor/haptics
    - @capacitor/keyboard
    - @capacitor/status-bar
    - @ionic/vue
    - @ionic/vue-router
    - vue
    - vue-router
- devDependencies
    - @capacitor/cli
    - @vue/cli-service
    - cypress
    - eslint
    - jest
    - typescript

これで大体どんなライブラリで構成されているのか分かりますね。

## Ionic
`@ionic/`から始まるのがIonicです。IonicはAndroidやiOSのネイティブなUIに似せたウェブコンポーネントのライブラリです。全てのコンポーネントに対してAndroid似とiOS似の二つの見た目が用意されていて、環境によってこの二つを使い分けてくれます。Ionic自体はただのウェブコンポーネントなのでIonicだけを使って見た目がiOSなパソコンブラウザ向けウェブアプリを作るとかもできます（使いづらいでしょうけど）。

## Capacitor
`@capacitor/`から始まるのがCapacitorです。Capacitorはモバイルアプリをウェブの技術で作るためのフレームワークです。HTML、JavaScript、CSSファイル等の形で与えられたUIを、デバイス上に立てたローカルサーバに配置しそれにアクセスするWebViewを表示するアプリを作ってあげることによって、ウェブ技術でモバイルアプリを構築します。そこにIonicのコンポーネントを使えばUIの見た目もネイティブに近づけることができるので、非常に相性が良いです。

## （IonicとCapacitorの関係性）
CapacitorとIonicは両方ともOSSですが、同じ企業が主導して開発されています。なのでIonicをscaffoldingでセットアップするとデフォルトでCapacitorもついてきます（Capacitorの周囲に有料サービスがあったりする）。しかし二つのライブラリに依存関係はなく、それぞれを独立して使うことができます。

## Vue
`vue`及び`vue-router`がご存知の通り動的なDOM操作を行うライブラリです。そして`@vue/cli-service`が`.vue`ファイルを含めアセットのトランスパイル、及びバンドルを行うパッケージです。これの中身はWebpackやBabelでできているようです。

## その他
残りはTypeScriptとリンター及びテストライブラリですね。

# Capacitorのセットアップ
以上で使われているライブラリの構成が分かったので、ここからいよいよコーディングになります。まずはCapacitorの必要要件を確認し、その後ウェブアプリを作成します。ウェブアプリをCapacitorに通せば最低限のモバイルアプリができます。

## Capacitorの必要要件
ウェブで作られたUIをCapacitorでモバイルアプリ化するための必要項目は[公式サイトのここ](https://capacitorjs.com/docs/getting-started#adding-capacitor-to-your-existing-web-application)に書かれており、かなりゆるいです。本稿でも引用すると

- `package.json` ファイル
- `dist` や `www` のようにビルドされたウェブアセットが入る、他とは分けられたディレクトリ
- ウェブアセットディレクトリの直下に配置された `index.html` ファイル

これさえあれば良いということなので、Ionicが必須ではないのはもちろん、ReactやVueやAngularも必須ではなく、これらのライブラリに依存するものではないということです。おそらくSPA(Single Page Application)ならほとんどどんな形のウェブサイトでもモバイルアプリ化できると思われます（`index.html`にコードを注入すると書いてあるのでSPAであることは必須である可能性があります）。

## ウェブアプリ作成
ということでまずはSPAのウェブアプリを作ります。ここではVueを使い、ビルドにはViteを用います。ここは一般的なVueの使い方と何も変わらないのでサクッといきます。最低限の1ページのみのアプリとします。

ディレクトリ構造は以下のようにします。

```
app/
+ dist/
+ src/
| + pages/
| | - PageIndex.vue
| + App.vue
| + main.js
| - router.js
+ index.html
+ package.json
- vite.config.js
```

それぞれのファイルの中身は

```vue:./src/pages/PageIndex.vue
<template>
<h1>My App</h1>
<input v-model="text" />
<p>Hello {{ text }} World!</p>
</template>
<script>
import { ref } from 'vue';

export default {
  name: 'PageIndex',
  setup() {
    const text = ref('Mobile App');
    return { text };
  },
}
</script>
```

```vue:./src/App.vue
<template>
<router-view />
</template>
<script>
export default { name: 'App' }
</script>
```

```js:./src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router.js';

const app = createApp(App);
app.use(router);  
app.mount('#app');
```

```js:./src/router.js
import { createRouter, createWebHistory } from 'vue-router';

const routes = [{
  path: '/',
  component: () => import('./pages/PageIndex.vue'),
}];
export default createRouter({
  history: createWebHistory(), routes,
});
```

```html:./index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My App</title>
    <script type="module" src="/src/main.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
```

```json:./package.json
{
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.39",
    "vue-router": "^4.1.5"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.1.0",
    "@vue/compiler-sfc": "3.2.39",
    "vite": "^3.1.0",
  }
}
```

```js:./vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'

export default defineConfig({ plugins: [vue()] });
```

長いですが普通のSPAを作るだけです。これで `$ npm run build` することで `./dist/`ディレクトリ以下にビルドされたウェブアセットが出力されます。また`$ npm run dev`することでWebブラウザ上で表示を確認できます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2354612/c0f69eb9-e4ce-b06f-98a5-d8b17e77353f.png)

## Capacitorの導入
Capacitorを導入するにあたって必要になるファイルは `capacitor.config.json` の一つだけです。その中身は以下です。

```js:./capacitor.config.json
{
  "appId": "com.example.app",
  "appName": "My App",
  "webDir": "dist",
  "bundledWebRuntime": false
}
```

`appId` はアプリストアに登録するときのアプリID、`appName` はアプリの名前です。この2つの設定は後でAndroidやiOS用のファイルを生成するときにデフォルト値として使うためのもののようです。`webDir` はビルド済みのウェブアセットが出力されるディレクトリを指定します。`bundledWebRuntime` はビルドシステムやバンドラーを使用していない場合には `true` にするそうです（詳細は理解してない）。普通は `vite` 等を使っているので `false` で良いはずです。

指定するのはバンドル後のウェブアセットのディレクトリだけなので、VueだろうがReactだろうがCapacitor的には関係ないことがわかりますね。

あとはCapacitorをインストールすれば導入は完了です。

```console
$ npm install @capacitor/core
$ npm install --save-dev @capacitor/cli
```

## モバイルアプリを生成
Capacitorを使って先ほど作ったSPAからAndroidやiOS用のモバイルアプリを生成します。ここはCapacitorの公式ドキュメントの手順と同じです。必要な開発環境についてのドキュメントは以下、

https://capacitorjs.com/docs/getting-started/environment-setup

コードベースへの導入方法はAndroidなら

https://capacitorjs.com/docs/android

iOSなら

https://capacitorjs.com/docs/ios

に書いてあります。公式ドキュメントに書いてあることなので本稿ではAndroidの場合のみ簡単に手順を追います。iOSの場合や詳細は公式ドキュメントを参照してください。

Androidの場合、開発環境としてAndroid Studioが必要なのでインストールしておきます。さらにAndroid Studioから必要なAndroid SDKを導入しておきます（詳しくは上の開発環境についてのドキュメント参照）。

コードベースにAndroid用のパッケージをインストールします。

```console
$ npm install @capacitor/android
```

Android用のコードを追加します。

```console
$ npx cap add android
```

これによってパッケージのルートに `./android/` ディレクトリが生成され、この中にはAndroid用のコードが含まれています。ここで`capacitor.config.json`の`appId`と`appName`が使われます。`./android/`ディレクトリの中身は一般的なAndroid Studioのプロジェクトになっているので、普通のAndroidアプリの開発と同じように開発できます。以下のコマンドでこのプロジェクトをAndroid Studioで開くことができます。

```console
$ npx cap open android
```

起動したAndroid Studioを使ってエミュレータで動かしたりPlayストア用のファイルを生成したり、普通のAndroidアプリとして開発できます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2354612/9470bc68-a745-ee72-ef79-6f17d2f9fd27.png)

コードを変更した場合は `$ npm run build` で ビルド済みアセットを `./dist/` ディレクトリに再生成した上で

```console
$ npx cap sync
```

を実行することで `./android/` ディレクトリ内のファイルも更新されます。

ここまででモバイルアプリをウェブの技術を使って作ることができました。Androidの場合を解説しましたがiOSの場合も似たような手順でアプリを生成できます（手順は上のリンク参照）。

# Ionicのセットアップ
ここまででモバイルアプリをウェブの技術を使って作ることができましたが、問題は見た目がダサいことです。各OSのネイティブなUIの見た目に近づけるためにIonicを使用します。IonicにはAngular版、React版、Vue版の他、それらライブラリを使わない[Web Component](https://developer.mozilla.org/ja/docs/Web/Web_Components)版もあります。今回はVue版を使います。

## Ionicの導入
最初にインストールしましょう。Vueの場合は以下です。

```console
$ npm install @ionic/vue @ionic/vue-router
```

`./src/router.js` に変更を加え、Ionicのルーターに置き換えます。import元が`from 'vue-router'` から `from '@ionic/vue-router'` になっただけです。

```diff_javascript:./src/router.js
@@ -1,4 +1,4 @@
-import { createRouter, createWebHistory } from 'vue-router';
+import { createRouter, createWebHistory } from '@ionic/vue-router';

const routes = [{
   path: '/',
```
`./src/main.js` にも変更を加えます。Vueのプラグインを追加するのと各種CSSを読み込むようにします。また、`app.mount()`を呼び出すタイミングが変わります。


```diff_javascript:./src/main.js
@@ -2,6 +2,31 @@
 import { createApp } from 'vue';
 import App from './App.vue';
 import router from './router.js';
 
+import { IonicVue } from '@ionic/vue';
+
+/* Core CSS required for Ionic components to work properly */
+import '@ionic/vue/css/core.css';
+
+/* Basic CSS for apps built with Ionic */
+import '@ionic/vue/css/normalize.css';
+import '@ionic/vue/css/structure.css';
+import '@ionic/vue/css/typography.css';
+
+/* Optional CSS utils that can be commented out */
+import '@ionic/vue/css/padding.css';
+import '@ionic/vue/css/float-elements.css';
+import '@ionic/vue/css/text-alignment.css';
+import '@ionic/vue/css/text-transformation.css';
+import '@ionic/vue/css/flex-utils.css';
+import '@ionic/vue/css/display.css';
+
const app = createApp(App);
-app.use(router);  
-app.mount('#app');
+app.use(IonicVue);
+app.use(router);
+
+router.isReady().then(() => {
+  app.mount('#app');
+});
```

これで導入は完了です。

## 基本的なIonicコンポーネントの使い方
Ionicが導入できたので、あとは基本的にはそれらのコンポーネントを組み合わせて画面を構成していくだけです。例えばリスト表示なら`<ion-list>`、ボタンなら`<ion-button>`といった感じです。各コンポーネントの動作するサンプルは以下の公式サイトに載っています。

https://ionicframework.com/docs/

またそれぞれのコンポーネントの説明は以下に載っています。

https://ionicframework.com/docs/components

ただ、Ionicにはページ遷移関係でいくつかお作法があり、これを守らないと表示がおかしくなる可能性があります。例えば、

- アプリのルートに`<ion-app>`コンポーネントを使わなければならない。
- ページ遷移で変わる部分には`<router-view>`の代わりに`<ion-router-outlet>`コンポーネントを使う。
- 各ページの内容のルートに`<ion-page>`コンポーネントを使う。
- ページ遷移のリンクは`<router-link>`コンポーネントで行う代わりに、任意のIonicコンポーネントに`router-link`属性をつけることによって行う。
- プログラムによるページ遷移を行うときは`import { useRouter } from 'vue-router'`の`useRouter()`代わりに`import { useIonRouter } from '@ionic/vue'`の`useIonRouter()`を使う。

などなど。この辺りのお作法は公式ドキュメントを参照してください。ドキュメントはVueは[ここ](https://ionicframework.com/docs/vue/navigation)、Reactなら[ここ](https://ionicframework.com/docs/react/navigation)に書いてあります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2354612/e78dfb63-7096-8b17-4a73-6320eb1e7473.png)

それっぽい見た目になりました。当然、Ionicのコンポーネントは他にもたくさんあるので、それらを組み合わせれば本格的なアプリも作れます。

# まとめ
IonicとCapacitorを使ったモバイルアプリをscaffoldingなしで自力で構築しました。これによってどんなライブラリがどんな依存関係で組み合わさって構成されているのかが分かりました。内部構造の理解が深まったことでドキュメントに載ってないことでも問題を分解して自力で解決できる力がついたと思います。