kintone(キントーン)は、業務などで手軽に使えるWebデータベースアプリを提供しています。

簡易的な操作は標準で提供されているビューワーアプリで十分ですが、
デバイスの機能を使ったり、こだわったインターフェースを使いたい時には、
独自のアプリを開発することになります。

今回はkintoneアプリの一つ、「院内掲示板アプリ」のデータをMonacaアプリとして呼び出し、操作する手順を解説します。

前編と後編の2つの記事に分けて解説します。

前編であるこの記事では、次の機能を作成していきます。

  1. アプリへのログイン
  2. 掲示板スレッドの一覧表示
  3. 掲示板へのデータ登録

また、アプリ開発の中では、いくつかの注意点もあるので、合わせて解説します。

また、プロジェクトのソースコードは、GitHubに配置しています。
https://github.com/monaca-samples/kintone-bbs

アプリの仕様について

今回のアプリで利用するフレームワークは、Framework7を利用しています。

ルーティングファイル(routes.js)にて、画面を次の通りに設定しています。

最初に読み込まれるのは、ログインフォームを表示するHome画面です。

// ファイル: js/routes.js
const routes = [
  {
    path: "/",
    url: "./index.html",
  },
  // ログイン
  {
    path: "/home",
    name: "Home",
    componentUrl: "./pages/home.html"
  },
  // スレッド一覧画面
  {
    path: "/threads",
    name: "Threads",
    componentUrl: "./pages/threads.html"
  },
  // スレッド詳細画面
  {
    path: "/threads/:id",
    name: "Thread",
    componentUrl: "./pages/thread.html"
  },
  // スレッド新規作成画面
  {
    path: "/form",
    name: "Form",
    componentUrl: "./pages/form.html"
  },
  // Default route (404 page). MUST BE THE LAST
  {
    path: "(.*)",
    url: "./pages/404.html",
  },
];

index.htmlでは 、Framework7のViewとライブラリdayjsを読み込んでいます。

// ファイル: www/index.html
<div id="app">
    <!-- Views/Tabs container -->
    <div class="views tabs safe-areas">
      <!-- Your main view/tab, should have "view-main" class. It also has "tab-active" class -->
      <div id="view-home" class="view view-main">
      </div>
    </div>
  </div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.7/dayjs.min.js" integrity="sha512-bwD3VD/j6ypSSnyjuaURidZksoVx3L1RPvTkleC48SbHCZsemT3VKMD39KknPnH728LLXVMTisESIBOAb5/W0Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

kintoneの変数定義

以下の3つの変数が、kintone操作用の変数です。

これは js/app.js の中で定義しています。

KINTONE_DOMAIN、KINTONE_APP_IDはそれぞれ自身のものに書き換えてください。

AUTHは認証用として用いるので、ログイン時に内容を設定する変数になります。

// ファイル: js/app.js
const KINTONE_DOMAIN = "xxxx.cybozu.com";
const KINTONE_APP_ID = "1";
let AUTH; // 認証用

ログイン

ログイン画面

ログインは pages/home.html に実装しています。

kintoneでは、最初にID/パスワードによるログインを行います。

この時、認証だけを行うAPIはないため、ログイン情報を使って任意のAPI(今回はアプリ情報を取得するAPI)を実行します。

その結果が返ってくるかどうかで、認証の成否を判定しています。

認証がうまくいったら、一覧の画面に遷移しています。

// ファイル: www/pages/home.html
// ログインボタンを押した時の処理
const login = async () => {
    // 入力値を受け取る
    const username = $f7.$el.find('#username').val();
    const password = $f7.$el.find('#password').val();
    // APIリクエスト用のヘッダー文字列作成
    AUTH = ${username}:${password};
    try {
        // APIリクエスト
        const res = await getApp({id: KINTONE_APP_ID});
        // レスポンスで認証情報の正しさを判定
        const text = res.data.appId === KINTONE_APP_ID ? 'ログインしました' : res.message;
        // トースト表示
        $f7.toast.create({
            text,
            position: 'top',
            closeTimeout: 500,
        }).open();
        // 一覧画面に遷移
        $f7router.navigate({
            name: 'Threads',
        });
    } catch (e) {
        console.log(e.message);
    }
};

getApp 関数は js/app.js に定義しています。

その他、アプリ内で共通で利用する関数として、sendRequestgetHeaderspromisify を用意しています。

// ファイル: js/app.js
// アプリ情報取得関数
const getApp = ({ id }) => {
  const options = {
    method: 'get',
    params: { id },
  };
  return sendRequest(/app.json, options);
}

// リクエスト送信用共通関数
const sendRequest = (path, options, type = 'json', responseType = 'json') => {
  cordova.plugin.http.setDataSerializer(type);
  options.headers = getHeaders();
  options.responseType = responseType;
  return promisify(cordova.plugin.http.sendRequest)(
    https://${KINTONE_DOMAIN}/k/v1${path}, options
  );
}

// kintoneリクエスト用の共通ヘッダー作成関数
const getHeaders = () => {
  return {
    'X-Cybozu-Authorization': btoa(unescape(encodeURIComponent(AUTH)))
  }
}

// コールバック方式をPromise形式に変換する関数
function promisify(cdvFunc) {
  return function() {
      return new Promise((res, rej) => {
          cdvFunc(...arguments, res, rej);
      });
  }
}

スレッド一覧の取得

掲示板スレッドの一覧を表示する画面は、 pages/threads.html に記述しています。

スレッド一覧画面

kintoneからスレッド一覧を取得するには、レコードの取得(GET) – cybozu developer networkを利用します。

// ファイル: www/pages/threads.html
let records = []; // スレッド一覧用
// 画面が表示されたら呼び出されるイベント
$on("pageAfterIn", async (e, page) => {
    // スレッド一覧の取得
    const res = await getRecords({
        app: KINTONE_APP_ID,
    });
    // 結果を変数に入れる
    records = res.data.records;
    // 画面を更新
    $update();
});

getRecords 関数は js/app.js に記述しています。

// ファイル: js/app.js
// レコード情報取得関数
const getRecords = (params) => {
  const options = {
    method: 'get',
    params,
  };
  return sendRequest(/records.json, options);
}

データを取得したら、 $update() で描画を更新しています。

描画するHTML側では、スレッド一覧をtableタグを利用して表示しています。

// ファイル: www/pages/threads.html
<table>
    <thead>
        <tr>
            <th>#</th>
            <th>配信日</th>
            <th>タイトル</th>
            <th>重要度</th>
        </tr>
    </thead>
    <tbody>
        ${records.map(r => $h`
            <tr @click=${() => click(r)}>
                <td>${r.$id.value}</td>
                <td>${r.pubDate.value}</td>
                <td>${r.title.value}</td>
                <td>${r.level.value}</td>
            </tr>
        `)}
    </tbody>
</table>

行をタップした際には、 click 関数を呼び出しています。

// ファイル: www/pages/threads.html
// スレッド一覧をタップした際のイベント
const click = (record) => {
    // スレッド詳細画面に遷移
    $f7router.navigate(/threads/${record.$id.value}, {
        props: {
            record,
        },
    });
}

データの新規作成

掲示板の新規登録画面

データの新規作成は、 /form へ画面遷移します。

これは pages/threads.html のヘッダーにある「+」アイコンをタップした際のリンク先です。

// ファイル: pages/threads.html
<div class="right">
    <a href="/form" class="link">
        <i class="f7-icons">plus</i>
    </a>
</div>

フォームについて

掲示板の登録フォーム

pages/form.html の内容です。

ここではフォームを表示して入力を行います。

掲示板のタイトルやスレッド内容のテキストや日付、ファイルを指定できます。

// ファイル: www/pages/form.html
<form id="report">
    <div class="list no-hairlines-md">
        <ul>
            <li class="item-content item-input">
                <div class="item-inner">
                    <div class="item-input-wrap">
                        <input type="text" name="title" placeholder="掲示板のタイトル" value="掲示板のタイトル" />
                        <span class="input-clear-button"></span>
                    </div>
                </div>
            </li>
            <li class="item-content item-input">
                <div class="item-inner">
                    <div class="item-input-wrap">
                        <input type="date" name="pubDate" placeholder="2022-02-10" value="2022-02-10" />
                    </div>
                </div>
            </li>
            <li class="item-content item-input">
                <div class="item-inner">
                    <div class="item-input-wrap">
                        <select name="level">
                            <option value="通常" selected>通常</option>
                            <option value="至急">至急</option>
                            <option value="重要">重要</option>
                        </select>
                    </div>
                </div>
            </li>
            <li class="item-content item-input">
                <div class="item-inner">
                    <div class="item-input-wrap">
                        <textarea name="body" class="resizable"></textarea>
                    </div>
                </div>
            </li>
            <li class="item-content item-input">
                <div class="item-inner">
                    <div class="item-input-wrap">
                        <input type="file" name="attachment" multiple />
                    </div>
                </div>
            </li>
            <li>
                <a href="#" @click=${save} class="item-link list-button">保存する</a>
            </li>
        </ul>
    </div>
</form>

そして、保存ボタンを押すと save 関数が呼ばれます。

// ファイル: www/pages/form.html
// 新しいスレッドを保存するイベント
const save = async (e) => {
    // フォームデータをkintoneフォーマットに整形(加えてファイルのアップロード)
    const params = await formToObject($(e.target).parents("form")[0]);
    try {
        // リクエストデータの作成
        const data = {
            app: KINTONE_APP_ID,
            record: params
        }
        // レコードの登録
        const res = await addRecord(data);
        // データが登録できればメッセージ表示
        const text = res.data.revision ? "保存しました" : res.message;
        $f7.toast.create({
                text,
                position: "top",
                closeTimeout: 2000,
        }).open();
        $f7router.back();
    } catch (e) {
        console.log(e);
    }
};

save 関数には、入力データをkintoneのデータフォーマットに整形している formToObject 関数があります。

この関数では、ファイルのアップロードも行い、fileKeyというデータを取得しています。

// ファイル: www/pages/form.html
// フォームのデータをオブジェクトに変換する関数
const formToObject = async (form) => {
    const obj = {};
    const ary = Array.prototype.slice.call(form.elements);
    for (const ele of ary) {
        const { name, type, value } = ele;
        if (type !== "file") {
            obj[name] = { value };
            continue;
        }
        // ファイルのアップロード
        const promises = [];
        for (const file of ele.files) {
            promises.push(uploadFile(file));
        }
        const responses = await Promise.all(promises);
        // アップロードしたファイルをkintoneのデータフォーマットに整形
        obj[name] = {
            value: responses.map(response => {
                return {
                    fileKey: response.data.fileKey
                }
            }
        )};
    }
    return obj;
}

uploadFile 関数は js/app.js に定義しています。

// ファイル: js/app.js
// ファイルアップロード用関数
const uploadFile = (file) => {
  const data = new FormData();
  data.append('file', file, file.name);
  const options = {
    method: 'post',
    data,
  };
  return sendRequest(/file.json, options, 'multipart');
}

そして、データをオブジェクトにしたら、 addRecord 関数を使ってレコード登録しています。

この関数も js/app.js に定義しています。

// ファイル: js/app.js
// レコード登録用関数
const addRecord = (data) => {
  const options = {
    method: 'post',
    data,
  };
  return sendRequest(/record.json, options);
}

注意点

Cordovaプラグインを利用します

kintoneでは、CORS制限によってMonacaアプリからデフォルトのXMLHTTPオブジェクトを使った通信はできません。

代替として、プラグインのcordova-plugin-advanced-httpを利用します。

これは、ネイティブのネットワーク機能を使うCordovaプラグインになります。

インタフェースは専用のオブジェクトになっていて、XMLHTTPRequestとは互換性がないので注意してください。

kintone側のフィールド設定を更新

kintoneでは標準で日本語のフィールド名になっています。

このままだとJavaScriptからアクセスしづらいため、英語名でフィールドを更新します。

今回は次のように変更しています。

フィールド名 フィールドコード
タイトル title
重要度 level
配信内容 body
配信日 pubDate
関連資料 attachment

まとめ

kintoneアプリのデータを取得したり、登録したりするのは多少の癖がありますが、慣れれば難しくないでしょう。kintone公式のJavaScript SDKではCORSの制限があり利用できないので注意が必要です。

Monacaでアプリ開発をすることで、kintoneを拡張して外部システムと連携したり、スマホやタブレットで利用したりとkintone活用の幅を広げることができます。