Ionic + Stencil + CapacitorでWeb Componentsを使ったPWA & ハイブリッドアプリ開発

Ionicチームが開発しているStencilとCapacitorを組み合わせることで、Web Componentsを使ってハイブリッドのモバイルアプリ開発ができます。

「Stencilってなに?」「Capacitorってなに?」と、思われるかもしれません。 どちらも2019年にバージョン1.0系がリリースされたため、新しいプロダクトであると言えます。

ただ後発でリリースされたこともあり、かなり使いやすいです。 せっかくの機会ですので、本記事で導入周りを中心にご紹介いたします。

Ionic

Ionicはモバイルアプリケーションを開発するためのフレームワークです。 ハイブリッド向けのクロスプラットフォーム、およびPWAのために作られています。

ionicframework.com

フレームワークと書きましたが、基本的にはUIフレームワークとしての側面が強く、モバイルアプリケーションをWEBの技術で開発します。

Ionic Frameworkは、Webテクノロジー(HTML、CSS、JavaScript)を使って、高性能かつ高品質なモバイルとデスクトップアプリケーションをつくるためのオープンソースのUIフレームワークです。

https://ionicframework.com/jp/docs/intro より

Stencil

Stencilを一言で述べるならば、ウェブコンポーネント(Web Components)のコンパイラです。 つまりStencilを使って、コンポーネント指向でハイブリッドアプリを開発するのが「Ionic + Stencil」の組み合わせです。

stenciljs.com

次の記事がStencilのリリース案内ですので、ご一読をオススメ致します。

ionicframework.com

ライブコンパイルを使った動作確認

Stencilには「--watch --serve」オプションを使った、ライブコンパイルが備わってます。

そのため普段はブラウザ上でライブコンパイルを活用して動作確認(開発)しつつ、ある程度の修正が完了したらコードをビルドして、CapacitorでAndroidやiOSへコードを転送(copy or sync)します。

TypeScriptとtsx

これまたStencilの特徴かと思うのですが、StencilではTypeScriptとtsx(TypeScript + JSX)を使ってコードを書きます。

import { Component, h } from '@stencil/core';

@Component({
  tag: 'app-home',
  styleUrl: 'app-home.css'
})
export class AppHome {

  render() {
    return [
      <ion-header>
        <ion-toolbar color="primary">
          <ion-title>Home</ion-title>
        </ion-toolbar>
      </ion-header>,

      <ion-content class="ion-padding">
        <p>
          Welcome to the PWA Toolkit. You can use this starter to build entire
          apps with web components using Stencil and ionic/core! Check out the
          README for everything that comes in this starter out of the box and
          check out our docs on <a href="https://stenciljs.com">stenciljs.com</a> to get started.
        </p>

        <ion-button href="/profile/ionic" expand="block">Profile page</ion-button>
      </ion-content>
    ];
  }
}

1行目で「@stencil/core」から、謎のhをimportしているのがとても気になります。 これはhyperscriptを表します。 hをimportしないとJSXがエラーになるので必須です。

Import { h } is required

The h stands for "hyperscript", which is what JSX elements are transformed into (it's the actual function executed when rendering within the runtime).

https://github.com/ionic-team/stencil/blob/master/BREAKING_CHANGES.md より

これは私の思い出話ですが、Stencilを1.0系にアップデートしたところ大量の「missing h」エラーが出たため、やたらと印象に残ってます(苦笑)。

JSX記法でIonicフレームワークのボタンやツールバーなどを呼び出しながら、コンポーネント指向で開発していくのがStencilの基本です。

Componentのライフサイクル

Stencilのコンポーネントには、私の感覚ではiOSのViewControllerやReact.jsに似たようなライフサイクルがあります。

  • componentWillLoad()
  • componentDidLoad()
  • componentWillRender()
  • componentDidRender()
  • componentWillUpdate()
  • componentDidUpdate()

特徴としてWill系のライフサイクルでは、async/awaitが使えます。 そのため非同期処理が終わるまで、レンダリングを遅らせるといった工夫ができます。

stenciljs.com

Component API

propsとstateはReact.jsのエンジニアであればお馴染みかもしれませんが、Stencilでは @Prop()と@State()などのデコレータが使えます。 @State()が付いたプロパティが変更された場合、Viewが自動的に再レンダリングされます。

stenciljs.com

基本的には素直なコンポーネントです。 まずはライフサイクルとデコレータが重要なので覚えます。

Capacitor

Capacitorを使うと、iOS・Android・Electron・Webがひとつのコードベースで開発できます。 Ionic Frameworkに最適化されており、Ionicと組み合わせて使うとかなり便利です。

capacitor.ionicframework.com

Capacitorには互換性があり、CordovaとIonicのネイティブプラグインが使えます。 私の印象としてはCordovaの後継としてリリースされたイメージなのですが、この理解であってるのでしょうか?

Capacitorも1.0系のリリースがとても新しく、2019年の5月です。

ionicframework.com

まずはプロジェクトを作成して実行しよう

長々と文章で書いても理解しにくいと思いますので、実際に試してみましょう。 お手軽に開発を始めるために、ベースのリポジトリとして「ionic-pwa-toolkit」を使います。

github.com

$ git clone https://github.com/ionic-team/ionic-pwa-toolkit ionic-pwa
$ cd ionic-pwa/
# ionic-pwa-toolkitの@ionic/coreが古くてエラーになるため、先にバージョンをあげます
$ npm install @ionic/core@4.8.1 --save
$ npm install
$ npm run start

> stencil-starter-project-name@0.0.1 start /private/tmp/ionic-pwa-toolkit
> stencil build --dev --watch --serve

[42:14.9]  @stencil/core v1.3.0 🌎
[42:16.2]  build, app, dev mode, started ...
[42:16.3]  transpile started ...
[42:25.8]  transpile finished in 9.48 s
[42:25.8]  type checking started ...
[42:25.9]  copy started ...
[42:26.1]  generate styles started ...
[42:26.2]  bundling components started ...
[42:34.0]  copy finished (704 files) in 8.17 s
[42:36.8]  type checking finished in 10.98 s
[42:38.1]  generate styles finished in 12.02 s
[42:39.3]  bundling components finished in 13.18 s
[42:39.5]  dev server: http://localhost:3333/
[42:39.5]  build finished, watching for changes... in
           23.25 s

ビルドが終わると、ブラウザは自動的に起動します。 または好きなブラウザで「localhost:3333」にアクセスすれば、動作確認できます。

f:id:konosumi:20190904003926p:plain

なお「npm run start」は、package.jsonに書いてある「"start": "stencil build --dev --watch --serve"」のことです。

私はserve状態でソースコードを修正しつつ、ブラウザで動作確認しながら開発を進めています。 ブラウザはクロームのデベロッパーツールを使い、サイズを適当なスマートフォンにしてそれっぽくしています。

package.json

「ionic-pwa-toolkit」ではscriptsにコマンドが設定されています。 それをそのまま利用することでstencilをbuildできます。

{
  "name": "stencil-starter-project-name",
  "private": true,
  "version": "0.0.1",
  "description": "stencil-starter-project-name",
  "license": "MIT",
  "files": [
    "dist/"
  ],
  "scripts": {
    "build": "stencil build",
    "start": "stencil build --dev --watch --serve",
    "test": "stencil test --spec --e2e",
    "test.watch": "stencil test --spec --e2e --watch",
    "generate": "stencil generate"
  },
  "dependencies": {
    "@ionic/core": "^4.8.1"
  },
  "devDependencies": {
    "@stencil/core": "^1.3.0"
  }
}

Capacitorの追加

「ionic-pwa-toolkit」にはCapacitorがないので追加します。 initするとアプリ名とアプリのパッケージIDを聞かれるので答えます。

$ npm install --save @capacitor/core @capacitor/cli
$ npx cap init
? App name App
? App Package ID (in Java package format, no dashes) com.example.app
✔ Initializing Capacitor project in /private/tmp/ionic-pwa in 5.48ms


🎉   Your Capacitor project is ready to go!  🎉

Add platforms using "npx cap add":

  npx cap add android
  npx cap add ios
  npx cap add electron

Follow the Developer Workflow guide to get building:
https://capacitor.ionicframework.com/docs/basics/workflow

ここで答えた内容は「capacitor.config.json」に保存されています。 後で変えたい場合は変更してください。

{
  "appId": "com.example.app",
  "appName": "App",
  "bundledWebRuntime": false,
  "npmClient": "npm",
  "webDir": "www"
}

AndroidとiOS用のプロジェクトを追加する

「npx cap add android」と「npx cap add ios」で、それぞれAndroidとiOSのプロジェクトを作成します。

Androidの追加

$ npx cap add android
✔ Installing android dependencies in 3.65s
✔ Adding native android project in: /private/tmp/ionic-pwa/android in 128.71ms
✔ Syncing Gradle in 39.99s
✔ add in 43.77s
✔ Copying web assets from www to android/app/src/main/assets/public in 682.02ms
✔ Copying native bridge in 1.48ms
✔ Copying capacitor.config.json in 1.15ms
✔ copy in 695.71ms
✔ Updating Android plugins in 1.35ms
  Found 0 Capacitor plugins for android:
✔ update android in 15.68ms

Now you can run npx cap open android to launch Android Studio

iOSの追加

iOSではcocoapodsが必要です。 iOSアプリ開発を経験しているエンジニアであれば、すでに入っていることも多いでしょうが、なければ追加します。

$ npx cap add ios
[error] cocoapods is not installed. For information: https://guides.cocoapods.org/using/getting-started.html#installation

# エラーになったのでcocoapodsを追加する
$ sudo gem install cocoapods

$ npx cap add ios
✔ Installing iOS dependencies in 3.88s
✔ Adding native xcode project in: /private/tmp/ionic-pwa/ios in 23.45ms
✔ add in 3.90s
✔ Copying web assets from www to ios/App/public in 514.54ms
✔ Copying native bridge in 1.91ms
✔ Copying capacitor.config.json in 8.32ms
✔ copy in 558.56ms
✔ Updating iOS plugins in 1.18ms
  Found 0 Capacitor plugins for ios:
✔ Updating iOS native dependencies with "pod install" (may take several minutes) in 1341.07s
✔ update ios in 1341.09s

Now you can run npx cap open ios to launch Xcode

stencilのbuildとモバイルアプリへのsync

まず始めにstencilをビルド(コンパイル)します。

$ npm run build

> stencil-starter-project-name@0.0.1 build /private/tmp/ionic-pwa
> stencil build

[06:54.4]  @stencil/core v1.3.0 🌎
[06:54.7]  build, app, prod mode, started ...
[06:55.2]  transpile started ...
[07:23.2]  transpile finished in 27.96 s
[07:23.2]  type checking started ...
[07:23.4]  copy started ...
[07:23.6]  generate styles started ...
[07:23.6]  bundling components started ...
[07:26.6]  copy finished (704 files) in 3.32 s
[07:47.0]  generate styles finished in 23.36 s
[07:52.6]  type checking finished in 29.34 s
[08:34.9]  bundling components finished in 71.21 s
[08:35.0]  build finished in 100.28 s

次にcapacitorを使って、androidとiosにビルドしたコードを転送すればOKです。

$ npx cap sync
✔ Copying web assets from www to android/app/src/main/assets/public in 1.47s
✔ Copying native bridge in 17.08ms
✔ Copying capacitor.config.json in 11.85ms
✔ copy in 1.57s
✔ Updating Android plugins in 7.25ms
  Found 0 Capacitor plugins for android:
✔ update android in 117.64ms
✔ Copying web assets from www to ios/App/public in 985.29ms
✔ Copying native bridge in 1.44ms
✔ Copying capacitor.config.json in 8.83ms
✔ copy in 1.01s
✔ Updating iOS plugins in 1.31ms
  Found 0 Capacitor plugins for ios:
✔ Updating iOS native dependencies with "pod install" (may take several minutes) in 9.07s
✔ update ios in 9.10s
✔ copy in 462.43μp
✔ update web in 9.07μp
Sync finished in 11.819s

「npx cap sync」と「npx cap copy」の違い

syncの場合はネイティブプラグインのアップデートも行われますが、copyでは単純にコードを転送します。

copyのほうが早いですが、プラグインの更新などを含める場合はsyncを使用すると覚えましょう。

$ npx cap copy
✔ Copying web assets from www to android/app/src/main/assets/public in 1.02s
✔ Copying native bridge in 2.19ms
✔ Copying capacitor.config.json in 1.89ms
✔ copy in 1.05s
✔ Copying web assets from www to ios/App/public in 542.82ms
✔ Copying native bridge in 1.14ms
✔ Copying capacitor.config.json in 1.59ms
✔ copy in 551.88ms
✔ copy in 391.00μp

アプリの実行

Androidアプリの実行

「npx cap open android」を実行すると、Android Studioでプロジェクトが開きます。 注意点としてはAndroid Studioがないと、アプリプロジェクトが開きません。

なおプロジェクトそのものは、ionic-pwa(プロジェクト直下)/androidディレクトリにあります。 これは「npx cap add android」が自動的に作成しました。

$ npx cap open android

オープンしたAndroid Studioでアプリを実行します。 なおAndroid Studioのバージョンが古いと、正常に実行できない可能性があります。 ご了承ください。

(再生ボタンよりシミュレーターを選択して実行すればOKです。なお純正のシミュレータは重いので、実際の開発で使われることは少ないですが。)

f:id:konosumi:20190904012302p:plain

iOSアプリの実行

「npx cap open ios」を実行すると、Xcodeでプロジェクトが開きます。 注意点としてはXcodeがないと、アプリプロジェクトが開きません。

なおプロジェクトそのものは、ionic-pwa(プロジェクト直下)/iosディレクトリにあります。 これは「npx cap add ios」が自動的に作成しました。

$ npx cap open ios

オープンしたXcodeでアプリを実行します。 なおXcodeのバージョンが古いと、正常に実行できない可能性があります。ご了承ください。

f:id:konosumi:20190904025358p:plain

ハイブリッドアプリはどうやって実行されるのか

iOS用に生成された、Xcodeプロジェクトを使って少しだけ追ってみました。

アプリ起動時に呼ばれるAppDelegate.swift

iOSアプリではアプリが起動すると、「AppDelegate.swift」が呼ばれます。 中でもdidFinishLaunchingWithOptionsが重要なのですが、ここは確認したところもぬけの殻でした。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
    return true
}

Storyboardに設定されたCAPBridgeViewController

次に「Main.storyboard」です。 Capacitorで生成したXcodeプロジェクトには、LaunchScreenとMainのStoryboardがあります。

「Main.storyboard」では、CAPBridgeViewControllerが最初に呼ばれるViewControllerとして指定されています。 CAPはCapacitorの略称かと思われますが、こいつが重要でブリッジの役割を果たします。

f:id:konosumi:20190904030013p:plain

CAPBridgeViewControllerがやっていること

CAPBridgeViewControllerを確認すると、WKWebViewを使っていることがわかります。 その上でViewControllerのviewを、webViewに差し替えていることがわかります。

つまりWKWebView上で、ハイブリッドアプリのJavaScriptが実行されていると解釈できそうです。(ここから先は夜も更けてしまい眠くて追えてないため、もしかしたら違うかもしれないですが。)

public class CAPBridgeViewController: UIViewController, CAPBridgeDelegate, WKScriptMessageHandler, WKUIDelegate, WKNavigationDelegate {
  
  private var webView: WKWebView?
  // 〜中略〜
  override public func loadView() {
    // 〜中略〜
    webView = WKWebView(frame: .zero, configuration: webViewConfiguration)
    webView?.scrollView.bounces = false

    webView?.scrollView.contentInsetAdjustmentBehavior = .never

    webView?.uiDelegate = self
    webView?.navigationDelegate = self
    webView?.configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
    // viewControllerのviewがwebViewに差し替わっている
    view = webView
  }

さいごに

StencilとCapacitorが新しいこともあり、かなり使いやすく設計されています。 とくに「npx cap add android」と「npc cap add ios」だけでモバイルアプリプロジェクトが作成できるので、ハイブリッドアプリ化する敷居が低いです。 さらにデスクトップアプリ版としてelectronにも使えます。

またcapacitor-fcm(Firebase Cloud Messaging向け)をはじめとするCapacitor向けのプラグインも、徐々に増え始めています。 Cordovaのプラグインはメンテナンスされてないものが多いという懸念点があったのですが、新しくCapacitor向けのプラグインも使っていくことで、ハイブリッドアプリ開発の選択肢としては十分にありだと感じています。

capacitor.ionicframework.com

StencilもCapacitorも1.0系のリリースが今年(2019年)に入ってからのため、変化の流動性がまだまだ起こり得るという点こそありますが。 新しいこともあり快適性は高いので、ぜひ試してみてはいかがでしょうか?

お知らせ

本記事との関連性はnpmやJavaScriptくらいしかないですが、技術書典7で「Node.js中級者を目指す」という同人誌を頒布します。

ご興味ある方はぜひ、当日の「このすみ堂」でお待ちしています。電子書籍版をBOOTHでも頒布予定です。

techbookfest.org