フロントエンドの開発領域では、Next.jsというフレームワークの人気が増しています。このフレームワークは、サーバーサイドのコードも生成できるため、特にWebアプリケーション開発に多く利用されています。

さて、そのNext.jsを使ってモバイルアプリを開発するという新しいアプローチを取り上げた記事があります。その記事は、「Build Mobile Apps with Tailwind CSS, Next.js, Ionic Framework, and Capacitor - DEV Community」というタイトルで、このリンクから読むことができます。

この記事に基づき、モバイルアプリ開発でNext.jsを利用する方法について詳しく解説します。リポジトリ(プロジェクトの原型となるソースコード集)も提供しますので、具体的にどのように進めれば良いかを理解しやすくなるでしょう。

ソースコードについて

該当リポジトリはmlynch/nextjs-tailwind-ionic-capacitor-starterにあります。

利用技術について

この記事では、Monacaではなく、Ionic社が開発したIonic FrameworkとCapacitorを使用します。Capacitorについて深くは触れませんが、Cordovaと同じく、Web技術を用いてモバイルアプリを開発するツールと理解してもらえればと思います(CordovaとCapacitorは細部でいくつかの違いがあります)。

さらに、記事ではNext.jsと共に、最近よく話題になるTailwind CSSも使用します。

概要を簡単にまとめると、この記事で中心的に使用されている技術は以下のようになります。

使い方

この方法を使用するためには、まずNode.jsの開発環境を設定する必要があります。さらに、AndroidやiOSのアプリを開発するための環境もあなたのパソコンに構築しておく必要があります。

これらの準備が整ったら、初めのステップとしてリポジトリ(プロジェクトの元となるコード)をダウンロードまたはクローン(コピー)します。

git clone https://github.com/mlynch/nextjs-tailwind-ionic-capacitor-starter.git

ライブラリのインストール

npm コマンドを使ってライブラリをインストールします。

cd nextjs-tailwind-ionic-capacitor-starter
npm install

ビルド

コードのビルドと、それを out フォルダに出力します。

npm run build
npm run export

Capacitorプロジェクトにコードをコピー

out に出力された内容をCapacitorプロジェクトにコピーします。 cap はCapacitorのコマンドです。

npx cap sync

実行する

後はiOS、Androidそれぞれを選んで実行します。エミュレーターやシミュレーターも利用できます。

npx cap run ios
npx cap run android

実際の画面

こちらがiOSエミュレーターで実行した画面です。一部はネイティブのUIとなっています。そのためもあり、Safariなどでデバッグ接続はできません。

リスト表示や通知機能なども利用できます。

プロジェクトの構成

プロジェクトは以下のようなフォルダ構成となっています(一部)。

% tree -d -L 2
.
├── android
│     (Android用プロジェクトに関するファイル)
├── components
│   ├── pages
│   └── ui
├── ios
│     (iOS用プロジェクトに関するファイル)
├── mock (サンプルデータ)
├── out (コードの出力先)
├── pages (Next.jsのルーティング用。モバイルアプリでは利用せず)
├── public (画像などのアセットを入れる場所)
├── store (アプリ内の共通変数を操作する)
└── styles (CSSファイル)

実際のところ、 componentsstore 以下が大事なファイルと言った印象です。

最初のコンポーネント

最初は components/AppShell.jsx が読み込まれます。このファイルにコメントを付与したのが以下の内容です。

// 必要なIonic React, Capacitor, React Routerのライブラリをインポートします。
import { IonApp, IonRouterOutlet, setupIonicReact } from "@ionic/react";
import { StatusBar, Style } from "@capacitor/status-bar";
import { IonReactRouter } from "@ionic/react-router";
import { Redirect, Route } from "react-router-dom";

// "Tabs"という名前のページコンポーネントをインポートします。
import Tabs from "./pages/Tabs";

// Ionic Reactの初期設定を行います。
setupIonicReact({});

// システムがダークモードを好むかどうかを監視し、一致する場合はStatusBarのスタイルをダークに、そうでない場合はライトに設定します。
// この操作は非同期で行われ、エラーが発生した場合は無視されます。
window.matchMedia("(prefers-color-scheme: dark)").addListener(async (status) => {
  try {
    await StatusBar.setStyle({
      style: status.matches ? Style.Dark : Style.Light,
    });
  } catch {}
});

// AppShellという名前の関数コンポーネントを定義します。
const AppShell = () => {
  return (
    // Ionicアプリケーションのルートとなるコンポーネントを定義します。
    <IonApp>
      // Ionic用のReact Routerをセットアップします。
      <IonReactRouter>
        // 主要なルーティング出口を設定します。
        <IonRouterOutlet id="main">
          // "/tabs"パスにアクセスされた場合、Tabsコンポーネントを表示します。
          <Route path="/tabs" render={() => <Tabs />} />
          // ルートパス("/")にアクセスされた場合、"/tabs/feed"パスにリダイレクトします。exactはパスが完全に一致する場合のみ適用されることを指します。
          <Route path="/" render={() => <Redirect to="/tabs/feed" />} exact={true} />
        </IonRouterOutlet>
      </IonReactRouter>
    </IonApp>
  );
};

// AppShellコンポーネントをデフォルトエクスポートします。
export default AppShell;

このファイル内で読み込んでいる components/pages/tabs/jsx は以下のようになっています。このファイル内でルーティングも定義されています。

// 必要なReact Router, Ionic React, ioniconsのライブラリをインポートします。
import { Redirect, Route } from "react-router-dom";
import { IonRouterOutlet, IonTabs, IonTabBar, IonTabButton, IonIcon, IonLabel } from "@ionic/react";
import { cog, flash, list } from "ionicons/icons";

// 各タブで表示するページコンポーネントをインポートします。
import Home from "./Feed";
import Lists from "./Lists";
import ListDetail from "./ListDetail";
import Settings from "./Settings";

// Tabsという名前の関数コンポーネントを定義します。
const Tabs = () => {
  return (
    // IonTabsコンポーネントを使用してタブの枠組みを定義します。
    <IonTabs>
      // IonRouterOutletでルーティング出口を定義します。
      <IonRouterOutlet>
        // 各タブに対応するルートを設定します。
        <Route path="/tabs/feed" render={() => <Home />} exact={true} />
        <Route path="/tabs/lists" render={() => <Lists />} exact={true} />
        <Route path="/tabs/lists/:listId" render={() => <ListDetail />} exact={true} />
        <Route path="/tabs/settings" render={() => <Settings />} exact={true} />
        // "/tabs"にアクセスされた場合、デフォルトで"/tabs/feed"にリダイレクトします。
        <Route path="/tabs" render={() => <Redirect to="/tabs/feed" />} exact={true} />
      </IonRouterOutlet>
      // IonTabBarでタブバーを定義します。これは画面の下部に表示されます。
      <IonTabBar slot="bottom">
        // 各IonTabButtonでタブボタンを定義します。それぞれのタブはアイコンとラベルを持ちます。
        <IonTabButton tab="tab1" href="/tabs/feed">
          <IonIcon icon={flash} />
          <IonLabel>Feed</IonLabel>
        </IonTabButton>
        <IonTabButton tab="tab2" href="/tabs/lists">
          <IonIcon icon={list} />
          <IonLabel>Lists</IonLabel>
        </IonTabButton>
        <IonTabButton tab="tab3" href="/tabs/settings">
          <IonIcon icon={cog} />
          <IonLabel>Settings</IonLabel>
        </IonTabButton>
      </IonTabBar>
    </IonTabs>
  );
};

// Tabsコンポーネントをデフォルトエクスポートします。
export default Tabs;

各ルーティングに応じて、 pages 以下にあるファイルを読み込んでいます。

画面の移動(ルーティング)について

画面間の移動は次のように行われます。

<IonItem routerLink={/tabs/lists/${list.id}} className="list-entry">
  <IonLabel>{list.name}</IonLabel>
</IonItem>

上記のコードは、特定のリスト項目(IonItem)をクリックすると、その項目に関連付けられた新しい画面に移動します。

ここで {list.id} は各項目の固有のIDを表し、そのIDを使って特定の画面へのリンクを生成しています。

移動先の画面では、次のように特定のデータを取得します。このデータ取得の際に、「ストア」という機能が利用されています。ストアはアプリケーションの状態を管理するための仕組みで、アプリ内で共有されるデータを保存しています。

// Storeから全てのリストを取得します。このときuseStateとselectors.getListsを使って現在のリスト状態を把握します。
const lists = Store.useState(selectors.getLists);

// React RouterのuseParams()を使用し、現在のURLからパラメータを抽出します。
const params = useParams();

// 抽出したパラメータからlistId(リストのID)を取り出します。このIDは後で特定のリストを探す際に使います。
const { listId } = params;

// 取得した全リストから、IDがURLパラメータのlistIdと一致するものを探します。
// Array.prototype.findメソッドは、配列中の要素に対して条件をテストし、最初に条件に合致した要素を返します。
// この場合、リストのidがlistIdと一致するリストを探し、それをloadedListに格納します。
const loadedList = lists.find(l => l.id === listId);

selectors.getLists とは、一種のダミーデータ(テスト用の仮データ)のことです。このデータは mock/index.js というファイルに定義されています。しかし、実際にアプリケーションを作成する際には、このダミーデータの代わりに、インターネット経由で取得した実際のデータを使用することになるでしょう。

// Some fake lists
export const lists = [
  {
    name: "Groceries",
    id: "groceries",
    items: [{ name: "Apples" }, { name: "Bananas" }, { name: "Milk" }, { name: "Ice Cream" }],
  },
  {
    name: "Hardware Store",
    id: "hardware",
    items: [
      { name: "Circular Saw" },
      { name: "Tack Cloth" },
      { name: "Drywall" },
      { name: "Router" },
    ],
  },
  { name: "Work", id: "work", items: [{ name: "TPS Report" }, { name: "Set up email" }] },
  { name: "Reminders", id: "reminders" },
];

各コマンドについて

セットアップ時に buildexport などのコマンドがありましたが、あれはNext.jsの next コマンドになります。特別なコマンドを実行している訳ではありません。

npm dev -> next dev
npm build -> next build
npm start -> next start
npm export -> next export

開発について

開発を進める際には npm start というコマンドを使用します。このコマンドを実行すると、http://localhost:3000 のアドレスで自分のパソコン上にサーバーが立ち上がります。これにより、HTMLベースの表示をリアルタイムで確認しながら開発を進めることができます。この段階では、生成されているコードはすべてHTMLです。

ただし、注意点として、サーバーサイドのコードはスマートフォンアプリに移行した時には正常に機能しない可能性があるため、この点には注意が必要です。

できあがったファイルについて

npm build コマンドを使用すると、out フォルダ以下に静的なWebアプリケーションとして利用できる内容が出力されます。この出力された内容にCordovaを組み込むことで、Monacaアプリとしても利用することが可能になります。

まとめ

今回はNext.jsでモバイルアプリを開発するためのベースとなるプロジェクトmlynch/nextjs-tailwind-ionic-capacitor-starterを紹介しました。Next.jsを使ってWebアプリケーションを開発している人にとっては、同じ感覚でモバイルアプリを開発できるようになるでしょう。ぜひお試しください。

via Build Mobile Apps with Tailwind CSS, Next.js, Ionic Framework, and Capacitor - DEV Community