🔨

kintone と EDITROOM の機能連携の実例解説: 自作プラグインを利用した連携5―開発環境に React + MUI を導入する

2022/12/13に公開

目次

はじめに

アップデイティットの毛利です。

前回に引き続き、弊社の「EDITROOM」と kintone の自作プラグインを繋いで動作させるために作業を進めていきます。

前回はこちら:

作業

前回の記事で Svelte を導入して UI を実装しやすくする手段を確立しました。
今回は、念のため他のフレームワークでも正しく導入できるか確認も兼ねて、Svelte ではなく React を導入し、ついでに UI フレームワークの MUI もいれてみます。

React とは何?などの初歩的な内容は省略しますので、ご承知おきください。

本記事中の作業範囲

  1. Vite をベースに React を導入
    • Project は前々回の、Vite を入れ終わった直後から開始する。
  2. 設定画面の UI を修正
  3. アプリ画面の UI を修正
  4. MUI を導入してみる

なぜフロントエンドフレームワークを導入したいのか

こちらを参照ください。

開発

1. ライブラリの導入

まずは、 React を導入します。

React にも、 Vite をベースとしたテンプレートプロジェクトが存在します。
公式を参考に、テンプレートプロジェクトを呼び出して、依存関係を確認します。

> npm create vite@latest react_template_prj -- --template react-ts

上記結果の package.json をみると、次のようなプラグインが入ってました。
React のバージョンはこのまま v18 を使用します。

package.json(reactの初期テンプレートの方)
{
  ...(中略)
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.24",
    "@types/react-dom": "^18.0.8",
    "@vitejs/plugin-react": "^2.2.0",
    "typescript": "^4.6.4",
    "vite": "^3.2.3"
  }
}

typescript, vite は既に導入されているので、それ以外を入れる必要があるようです。
追加分を全て devDependencies に寄せますが、特に問題ありません。

> npm i -D react react-dom @types/react @types/react-dom @vitejs/plugin-react

次に、 tsconfig.json に設定を追加します。
ここでは、 React で使用する JSX 記法有効化する行を追加するだけです。

tsconfig.json
{
  "compilerOptions": {
    ...(中略)
    "newLine": "LF",
    "jsx": "react-jsx"
  },
  ...(中略)
}

次に、Vite に React を認識させるために vite.config.ts を修正します。

vite.config.ts
import react from '@vitejs/plugin-react'
...(中略)

export default defineConfig({
    ...(中略)
    plugins: [react()]
})

まず、この状態で、 Vite のビルドが通るか確認します(npm run build)。
問題なく通ったら、最初の導入は成功しました。ですが、まだ React のファイルを導入できていないので、引き続き進めます。

2. config の UI(html) を React に移植する

次の修正を入れます。(Svelte の際にもやった、描画先となる、div タグと id の制御を修正する作業です。)

  • ConfigUi.tsx に html の元々の構造を移植
  • config.html と config.ts を React 向けに修正.

早速、 React ファイルを導入していきましょう。

最初に、アップロードする html ファイルを修正します。
基本的に、 div タグ1つあれば事足りるため、そのように変更します。

static/html/config.html
<div id="config_react"></div>

次に、src 側を修正していきます。
ファイル数が少し増えますので、次のようなディレクトリ構成にしていきます。

  • src
    • config.tsx
    • desktop.tsx
    • [DIR]config
      • ConfigUi.tsx
    • [DIR]desktop
      • DesktopUi.tsx

ビルドの起点となった config.ts と desktop.ts についても、それぞれ .tsx に変換しそれぞれのディレクトリを用意してその中に新規ファイルを追加していきます。

まず、ConfigUi.tsx に対して config.html で元々あったタグを全部移植します。

config/ConfigUi.tsx
interface ConfigUiProps {
  kintone_plugin_id: string;
}

const ConfigUi: React.FC<ConfigUiProps> = (props) => {
  return (
    <section className="settings">
      <h2 className="settings-heading">Settings</h2>
      <form className="js-submit-settings">
        <p className="kintoneplugin-row">
          <label htmlFor="message">
            REST API URL:
            <input id="api_url" type="text" className="js-text-message kintoneplugin-input-text" />
          </label>
        </p>
        <p className="kintoneplugin-row">
          <label htmlFor="message">
            REST API KEY:
            <input id="api_key" type="text" className="js-text-message kintoneplugin-input-text" />
          </label>
        </p>
        <p className="kintoneplugin-row">
          <button className="kintoneplugin-button-dialog-ok">Save</button>
        </p>
      </form>
    </section>
  );
};

export default ConfigUi;

React の仕様として「class -> className」「for -> htmlFor」に読みかえる必要があるため、その部分を変更します。

次に、 config.ts の処理は一旦消して、React ファイルを描画する指示だけにします。

config.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import ConfigUi from "./config/ConfigUi";
import "@shin-chan/kypes";

const root = ReactDOM.createRoot(document.getElementById("config_react")!!);
root.render(
  <React.StrictMode>
    <ConfigUi kintone_plugin_id={`${kintone.$PLUGIN_ID}`} />
  </React.StrictMode>
);

Vite のビルド設定も変更します。

vite.config.ts
...(中略)
    rollupOptions: {
      input: {
        desktop: `${path.resolve(root, "src/desktop.tsx")}/`,
        config: `${path.resolve(root, "src/config.tsx")}/`,
      },
...(中略)

この3点を修正した後、一度 kintone にビルド結果をアップロードしてみましょう。

ぱっと見た感じ、html で作ったものと見た目が全く変わらないです。
――ですが、F12 で構造をみると、ちゃんと Svelte で作られていることが確認できます。

ただ、まだ移植は終わってないので引き続き進めていきます。

3. config.ts を移植する

UI の移植は成功したため、次は kintone と疎通している JavaScript の処理の箇所を分離しながら移植していきます。

さて、Svelte では TypeScript を記述する領域は予め決まっていました。
React は JavaScript(TypeScript) を拡張して記述する仕組み上、次作の関数や変数の置き場所について厳格なルールはありません。
書き方や置き方でパフォーマンスに多少なり影響は出ますが、今回は、呼び出しが可能なスコープ内にあれば良い、程度の感覚で進めてください。

config/ConfigUi.tsx
import "@shin-chan/kypes";
import { ChangeEvent, useCallback, useEffect, useState } from "react";

interface ConfigUiProps {
  kintone_plugin_id: string;
}

const ConfigUi: React.FC<ConfigUiProps> = (props) => {
  const [api_url, setApiUrl] = useState<string>("");
  const [api_key, setApiKey] = useState<string>("");

  const handleChangeUrl = useCallback((e: ChangeEvent<HTMLInputElement>) => setApiUrl(e.target.value), []);
  const handleChangeKey = useCallback((e: ChangeEvent<HTMLInputElement>) => setApiKey(e.target.value), []);

  const handleClick = useCallback(
    (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
      e.preventDefault();
      kintone.plugin.app.setConfig({ api_url, api_key }, () => {
        alert("The plug-in settings have been saved. Please update the app!");
        window.location.href = "../../flow?app=" + kintone.app.getId();
      });
    },
    [api_url, api_key]
  );

  useEffect(() => {
    const config = kintone.plugin.app.getConfig(props.kintone_plugin_id);
    setApiUrl(config.api_url);
    setApiKey(config.api_key);
    // -- Nothing Add DependencyList.
  }, []);

  return (
    <section className="settings">
      <h2 className="settings-heading">Settings</h2>
      <form className="js-submit-settings">
        <p className="kintoneplugin-row">
          <label htmlFor="message">
            REST API URL:
            <input id="api_url" type="text" className="js-text-message kintoneplugin-input-text" value={api_url} onChange={handleChangeUrl} />
          </label>
        </p>
        <p className="kintoneplugin-row">
          <label htmlFor="message">
            REST API KEY:
            <input id="api_key" type="text" className="js-text-message kintoneplugin-input-text" value={api_key} onChange={handleChangeKey} />
          </label>
        </p>
        <p className="kintoneplugin-row">
          <button className="kintoneplugin-button-dialog-ok" onClick={handleClick}>
            Save
          </button>
        </p>
      </form>
    </section>
  );
};

export default ConfigUi;

実装のアプローチはいくつかありますが、今回は一般的によく知られている useEffect で useState に流し込むスタイルで実装しました。
Svelte に比べると記述量が多いですが、こちらでも HTML タグを TypeScript の領域内で集中させられることがわかります。

ちなみに、 React 18 から Strict かつ開発モードにおいて useEffect が2回発火される仕様になっているそうです。
これは意図的に変更された仕様のため、もし開発中に 2 回 useEffect が呼ばれておかしくなった場合、実装が悪いということなので、気を付けてほしいです。

この状態で、再度プラグインをアップロードして、保存の処理まで問題なく動作するのを確認できたら、完成です。

4. desktop.ts の移植

先ほどのやり方を踏襲して、アプリ部(desktop.ts)も React に置き換えていきます。

こちらは html 部がなく、UI 構成もすべて JavaScript 領域で構成されているため、UI と処理を分離するため、少し手直しが必要になります。

desktop.tsx
import React from "react";
import ReactDOM from "react-dom/client";

import "@shin-chan/kypes";
import DesktopUi from "./desktop/DesktopUi";

const main = (kintone_plugin_id: string) => {
  kintone.events.on(["app.record.index.show"], (_event) => {
    const spaceElement = kintone.app.getHeaderSpaceElement();
    if (!spaceElement) throw new Error("The header element is unavailable on this page");
    const root = ReactDOM.createRoot(spaceElement);
    root.render(
      <React.StrictMode>
        <DesktopUi kintone_plugin_id={kintone_plugin_id} />
      </React.StrictMode>
    );
  });
};

((PLUGIN_ID) => main(PLUGIN_ID))(`${kintone.$PLUGIN_ID}`);
desktop/DesktopUi.tsx
import "@shin-chan/kypes";
import { useCallback, useMemo, useState } from "react";

interface DesktopUiProps {
  kintone_plugin_id: string;
}

const DesktopUi: React.FC<DesktopUiProps> = (props) => {
  const [buttonDisabled, setButtonDisabled] = useState<boolean>(false);

  const config = useMemo(() => {
    const config = kintone.plugin.app.getConfig(props.kintone_plugin_id);
    const { api_url, api_key } = config;
    return { api_url, api_key };
    // -- Nothing Add DependencyList.
  }, []);

  const handleClick = useCallback(
    (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
      setButtonDisabled(true);
      const { api_url, api_key } = config;
      //
      const header = { Accept: "application/json", "X-EDITROOM-API-KEY": api_key, "Content-Type": "application/json; charset=UTF-8" };
      const body = JSON.stringify({ docflow_name: "docflow0001", export_type: "html" }, null, 0);
      kintone.proxy(
        api_url,
        "POST",
        header,
        body,
        (body, status, headers) => {
          // success
          console.log(status, JSON.parse(body), headers);
          alert("sent.");
          setButtonDisabled(false);
        },
        (error) => {
          // error
          console.log(error); // proxy APIのレスポンスボディ(文字列)を表示
          alert("send error.");
        }
      );
    },
    [config]
  );

  return (
    <p className="kintoneplugin-row">
      <button disabled={buttonDisabled} onClick={handleClick} className="kintoneplugin-button-dialog-ok">
        REST API FIRE
      </button>
    </p>
  );
};

export default DesktopUi;

さて、前回の記事を読んでいただけている方は既に察していることでしょう。
――React を使用するファイルが複数現れたため、 React がビルド結果から切り出されるようになりました。

dist/out/js/desktop.js                    1.01 KiB / gzip: 0.62 KiB
dist/out/js/config.js                     1.45 KiB / gzip: 0.66 KiB
dist/out/assets/jsx-runtime.a2c3092f.js   139.10 KiB / gzip: 44.68 KiB
built in 248ms.

この jsx-runtime.js が react ライブラリそのものです。キレイに分離してます。
もちろん、このままで動かないので、対策をします。

5. (力技) コンフィグファイルを分離する

やっぱり解決策が見つからなかったため、Svelte の時同様にコンフィグファイルを2つに分離して、それぞれ config と desktop をビルドします。

Svelte の時にしたように vite_config.config.ts と、全く同じやり方でコピぺ修正した vite_desktop.config.ts をそれぞれ配置、package.json を修正、npm-start.js も修正します。

やってることは Svelte の時と全く同じなので割愛します。

また、React と ReactDOM は CDN 等から単独のファイルとして取ってくることもできます。
なので、CDN から配信することを想定して React 関係一式をビルド結果から切り出してしまえると良いのですが、kintone 側の制約と Vite の処理系との相性問題から、一旦断念しました。

おそらくですが、仮に Module としてライブラリ化したファイルを作成しても、kintone 側が module として認識しないので、うまく動かない気がします。(要検証)

6. MUI の導入

kintone には、 kintone 専用の CSS セットが存在します。
そのため、基本的に UI 設計を進めるうえでは、 kintone の CSS を充てていくことが統一感を出すうえで良い選択肢となります。

一方で、使い慣れた UI ツールがあるのであれば、そちらを使う方がより想定通りの UI を出力できます。
特に、グラフやテーブルに作りこみをしたい時、ある程度 UI ツールに依存しておくとかなり楽ができます。このあたりの選択は、悩ましいところです。

今回は、試しに MUI (Material-UI v5) を導入してちゃんと想定通りに UI が変化するか確認してみます。

公式の解説で必要とされる3つのライブラリをインストールします。

npm i -D @mui/material @emotion/react @emotion/styled

片っ端から置き替えることは出来ますが、とりあえず今回は、button タグに絞って変更してみます。

config/ConfigUi.tsx
import Button from "@mui/material/Button";
...(中略)
        <p className="kintoneplugin-row">
          <Button variant="contained" onClick={handleClick}>
            Save
          </Button>
        </p>
...(中略)

desktop/DesktopUi.tsx
import Button from "@mui/material/Button";
...(中略) 
    <p className="kintoneplugin-row">
      <Button variant="contained" disabled={buttonDisabled} onClick={handleClick}>
        REST API FIRE
      </Button>
    </p>
 
...(中略)

UI が正しく置き換わりました。
これで、作業は終了です。お疲れ様でした。

バンドルサイズについて

今回のようなプラグイン開発の範疇であれば、あまり気にしないところではありますが、一応ファイルサイズにどれだけの差が生じるかを把握しておきましょう。

比較するプロジェクトは、ViteとTypeScript導入直後の状態をベースに、各フレームワーク向けにビルドコンフィグとソースコードを最低限修正したもので、比較しました。
修正のやり方で多少なり結果がブレますので、あくまで参考値程度と考えてください。

ちなみに、ファイルサイズの次の順に小さくなっていきます。

React@18.2.0+MUI5 > React@18.2.0 > Vue@3.2.41 > Preact@10.11.3 > Solid@1.6.3 > Svelte@3.53.1

React+MUI
dist/out/js/config.js   209.27 KiB / gzip: 69.73 KiB
dist/out/js/desktop.js   208.81 KiB / gzip: 69.77 KiB
React
dist/out/js/config.js   140.44 KiB / gzip: 45.18 KiB
dist/out/js/desktop.js   139.98 KiB / gzip: 45.12 KiB
Vue で同等の内容をビルドした結果
dist/out/js/config.js   52.51 KiB / gzip: 21.15 KiB
dist/out/js/desktop.js   50.69 KiB / gzip: 20.48 KiB
Preact で同等の内容をビルドした結果
dist/out/js/desktop.js   22.33 KiB / gzip: 8.70 KiB
dist/out/js/config.js   22.71 KiB / gzip: 8.72 KiB
Solid で同等の内容をビルドした結果
dist/out/js/config.js   8.51 KiB / gzip: 3.50 KiB
dist/out/js/desktop.js   8.10 KiB / gzip: 3.45 KiB
Svelte で同等の内容をビルドした結果
dist/out/js/config.js   4.55 KiB / gzip: 2.07 KiB
dist/out/js/desktop.js   3.91 KiB / gzip: 1.90 KiB

一応、(超雑な)補足をすると、Svelte が「Vue 記法 + コンパイル」ならば Solid は「React 記法 + コンパイル」でファイルサイズを削減できるフレームワーク(むしろコンパイラツール)です。
Preact は React とほとんど同じ書き方で開発できて、かつ軽量なフレームワークです(一応 React 互換としてビルドを通せますが、互換させるためにはいろいろ御作法の修正が必要になります)。

現状、React 一式のファイル群が付いてくるため、どうしても 100KiB を越えてしまいます。(MUI までバンドルすると 200kiB 越え..)
既にいろいろなところでよく言われている話ですが、他のフレームワークと比べてしまうと、ちょっと気になるファイルサイズになってきます。

ただ、今回はあくまで kintone プラグイン開発が主旨のため、ベースサイズの差だけで React や Vue が選択肢から消えることはありません。
というのも公式(2022/12/09現在)には「ファイルサイズ 20MB まで」と記載されており、かなり緩い上限値になっているのでそこまで深刻な問題にはなりません。

改めて、こと kintone プラグイン開発においては、使い慣れたものか、使いたいライブラリが使えるものを採用することをお勧めします。

所感

今回は、React を kintone プラグイン開発に導入する方法を解説しました。
ブラウザ向けにおいては Next.js や Astro など配信サイドで React を最適化してから降らせるのが主流だと感じる昨今なので、今回のように1つのファイルに一式閉じ込める開発が必要になるケースは珍しいかもしれません。

そんな React ですが、JSX 記法の学習コストがあるため Vue/Svelte より使いこなすまでに時間がかかったり、そもそもの記述量が多めだったり、よく言われるデメリットは存在します。
ですがそれを踏まえても、自由度の高さと(JSX 記法のおかげで)より JavaScript に集中しやすい点は、本当に強いです。

また、個人的にですが、特に React + MUI の組み合わせは非常に素晴らしいものと考えています。

UI フレームワークの多くは、その自由度を損なわないよう CSS アセットとして提供されます。つまり、React や Vue、Svelte など特定のフレームワークに依存しません。
一方 MUI とその派生形の多くは、特定のフレームワーク専用として作られており、コンポーネントそのものを提供します。
そのため、CSS に疎くてもとりあえず JavaScript が書けさえすれば、ある程度パッとした見た目に仕上げることができる。これだけで、非常に素晴らしい仕組みだな、と思っています。

「ちょっと動くモックが必要だけど、プレーンな HTML だと味気なさすぎるし、良い感じに CSS を手を入れたら時間足りないし...」みたいなシチュエーションって、意外にあります。その時、 MUI を選択肢の 1 つとして、お勧めしたいです。

おわりに

再度の紹介となりますが、先日、弊社から「EDITROOM」という BtoB 向けの文書作成クラウドサービスをリリースしました。
特に、定期的に文書を作る人的コストや作業にかかる拘束時間の長さを改善するのに、このサービスが大きな一助になると自負しております。

もし、ご興味がありましたら、トライアルをご利用いただければ幸いです。

Discussion