Viteを使用したkintoneモバイルカスタム開発

# はじめに
Reactの開発環境の構築で、Create React Appの代わりにViteを第一の選択とするようにTheo氏が呼びかけました。
viteの影響力はvueからreactに広がっており、フロントエンドのエンジニアリング開発において、ますます人気が出てきています。webpackからviteに切り替える時期が来たと言えるでしょう。

# なぜviteを使用するのか
ViteはES Moduleに基づく開発サーバーを採用しています。
ローカルで開発する際には、HMRを使用することで速度が大幅に向上します。webpackと比較して、多くの利点があります：
1. より高速なホットアップデートメカニズム
2. より高速なパッケージング効率
3. よりシンプルな設定

kintoneの開発を行う際には、viteで開発、構築を行う記事について調べてみました。
vite（rollup）を使用してkintoneのカスタムjsをパッケージ化する方法を紹介する記事はありましたが、
viteを使用してkintoneの開発を行う方法についての記事は見つけられませんでした。そこで、この問題を解決するviteプラグインを開発しました。

## vite-plugin-kintone-dev
https://www.npmjs.com/package/vite-plugin-kintone-dev

このプラグインの機能：
1. viteを使用してkintoneのカスタムjsを作成し、hmrを使用して開発を高速化
2. react、vueなどの異なるフロントエンドフレームワークをサポート
3. ビルド時にパッケージングと自動アップロードkintoneをサポート

# 実践：kintoneのモバイル版のカスタマイズ
今回はviteプラグインと組み合わせて、kintoneのモバイル版のカスタム開発を例に解説します。
完成イメージはこちらです：[完成イメージ](#完成イメージ)
技術スタック：vite4 + vue3 + vant4

## 1. viteのスケルトンを使用してvueプロジェクトを初期化
まず、viteのスケルトンツールを使用してvueプロジェクトを作成します。
```sh
npm create vue@latest
```
プロジェクト名を設定します： kintone-mobile-custom（任意のプロジェクト名を設定してください）
vue、TypeScriptを選択し、要件に応じて選択します。そして初期化インストールを行います。
```sh
cd kintone-mobile-custom
npm install
```

## 2. kintone開発のviteプラグインをインストール
```sh
npm install -D vite-plugin-kintone-dev
```
初回起動時に、自動的にenvファイルの設定テンプレートをチェックします。
設定がない場合は、コマンドラインインタラクションを起動して設定情報を入力させ、自動的にenvファイルを更新します。
（serveモードでは ".env.development" ファイル、buildモードでは ".env.production" ファイル）

### プラグインの説明
```json
{
   outputName: "mobile",   //最終的なパッケージの名前
   upload: true
}
```

### viteの設定
```ts:vite.config.ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import kintoneDev from "vite-plugin-kintone-dev";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    kintoneDev({
      outputName: "mobile",
      upload: true
    }),
  ],
});
```

## 3. 他のライブラリのインストール
### 画像ライブラリ
次に、画像ライブラリを設定します。
非常によく使われるプラグイン画像ライブラリUnplugin Icons
https://github.com/unplugin/unplugin-icons
```sh
npm install -D unplugin-icons unplugin-vue-components
```
その具体的な設定は、公式ウェブサイトを参照してください。

すべてのiconリソースを追加しますが、実際のパッケージング時には、必要に応じてロードされます。
```sh
npm i -D @iconify/json
```

### モバイルUIライブラリ
次に、vantというライブラリを使用して、モバイル開発のUIライブラリとします。
https://vant-ui.github.io/vant/#/en-US
```sh
npm install vant
```
### tsconfig.app.jsonの設定
```json:tsconfig.app.json
{
    ...
    "compilerOptions": {
        "types": ["unplugin-icons/types/vue"],
        ...
    }
    ...
}
```
### viteの最終設定
```ts:vite.config.ts
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";
import Components from "unplugin-vue-components/vite";
import { FileSystemIconLoader } from "unplugin-icons/loaders";
import kintoneDev from "vite-plugin-kintone-dev";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    kintoneDev({
      platform: "PORTAL",
      type: "MOBILE",
    }),
    vue(),
    Components({
      resolvers: [IconsResolver()],
    }),
    Icons({
      compiler: "vue3",
      customCollections: {
        "my-icons": FileSystemIconLoader("./src/assets/icons"),
      },
    }),
  ],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});
```

# プログラム
## 1. main.tsの修正
Vueを初期化し、そのルートノードをモバイルのポータルの上部の空白部分の要素にマウントします
```ts:src/main.ts
import 'vant/lib/index.css'
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

import { Tabbar, TabbarItem } from 'vant'

kintone.events.on('mobile.portal.show', (event) => {
  const app = createApp(App)
  app.use(router)
  app.use(Tabbar)
  app.use(TabbarItem)
  app.mount(kintone.mobile.portal.getContentSpaceElement()!)
  return event
})
```

## 2. 元のモバイル画面を隠す
```css:src/assets/main.css
.gaia-mobile-v2-portal-announcement-container,
.gaia-mobile-v2-portal-appwidget-container,
.gaia-mobile-v2-portal-spacewidget-container {
  display: none;
}
.van-hairline--top-bottom::after,
.van-hairline-unset--top-bottom::after {
  border-width: 0;
}
.gaia-mobile-v2-viewpanel-header {
  background-color: #4b4b4b;
}
.gaia-mobile-v2-viewpanel-contents {
  border-radius: 0;
}
.van-tabbar {
  width: 100vw;
}
.van-tabbar--fixed {
  left: unset;
}
.gaia-mobile-v2-portal-header-container .gaia-mobile-v2-portal-header::after {
  background: none;
}
.group-module-background {
  background-color: rgba(255, 255, 255);
  border-radius: 10px;
  box-shadow: 0 0 5px 0 #ced3d4;
}
```

## 3. tabbarの追加
```vue:src/App.vue
<script setup lang="ts">
import { ref } from "vue"
import type { Ref } from 'vue'

const active: Ref<number> = ref(0)
</script>

<template>
  <router-view v-slot="{ Component }">
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </router-view>

  <van-tabbar route v-model="active" active-color="#febf00" inactive-color="#b8b8b5">
    <van-tabbar-item replace to="/">
      <span>Home</span>
      <template #icon="props">
        <i-mdi-home class="tabbar-icon" />
      </template>
    </van-tabbar-item>
    <van-tabbar-item replace to="/contacts">
      <span>Contacts</span>
      <template #icon="props">
        <i-mdi-contacts class="tabbar-icon" />
      </template>
    </van-tabbar-item>
    <van-tabbar-item replace to="/space">
      <span>Space</span>
      <template #icon="props">
        <i-mdi-shape-circle-plus class="tabbar-icon" />
      </template>
    </van-tabbar-item>
    <van-tabbar-item replace to="/star">
      <span>Star</span>
      <template #icon="props">
        <i-mdi-star class="tabbar-icon" />
      </template></van-tabbar-item>
    <van-tabbar-item replace to="/todo">
      <span>Todo</span>
      <template #icon="props">
        <i-mdi-tooltip-edit class="tabbar-icon" />
      </template></van-tabbar-item>
  </van-tabbar>
</template>

<style scoped>
.tabbar-icon {
  font-size: 1em;
}
</style>
```

## 4. デモアプリの作成
kintone上でアプリケーションを作成します。そして、以下のフィールドを準備します。

| フィールド名 | フィールドコード | Type         |
| ------------ | ---------------- | ------------ |
| title        | title            | 文字列 (1行) |
| num          | num              | 数値         |
| desc         | desc             | 文字列 (1行) |
| price        | price            | 数値         |
| thumb        | thumb            | 文字列 (1行) |

次に、データを追加してください。そして、【APIトークン】を設定してください。

## 5. kintone js sdkのインストール
```sh
npm install @kintone/rest-api-client 
```

## 6. kintoneのts宣言の追加
1. kintoneのts生成ツールのインストール
```sh
npm install -D @kintone/dts-gen
```
2. アプリケーションに基づいてts宣言を生成する
自分の環境に合わせて入力してください
```sh
npx @kintone/dts-gen --base-url https://xxxx.cybozu.com -u  xxxx -p xxxx --app-id xx
```
3. "tsconfig.app.json"の設定   
```ts:tsconfig.app.json
{
  ...
  "files": ["./node_modules/@kintone/dts-gen/kintone.d.ts"],
  ...
}
```
4. REST API の応答データに対する ts タイプの追加   
```ts:types/restApiRecords.ts
import { KintoneRecordField } from '@kintone/rest-api-client'

export type GoodListAppRecord = {
  $id: KintoneRecordField.ID
  $revision: KintoneRecordField.Revision
  thumb: KintoneRecordField.SingleLineText
  num: KintoneRecordField.Number
  title: KintoneRecordField.SingleLineText
  price: KintoneRecordField.Number
  desc: KintoneRecordField.SingleLineText
  更新人: KintoneRecordField.Modifier
  创建人: KintoneRecordField.Creator
  更新时间: KintoneRecordField.UpdatedTime
  记录编号: KintoneRecordField.RecordNumber
  创建时间: KintoneRecordField.CreatedTime
}
```
5.kintone SDK：@kintone/rest-api-client@4.1.0のTSのバグへの対処   
@kintone/rest-api-clientの4.1.0にはバグがあり、Viteの新しいバージョンでは、モジュール解析部分にTypeScript 5の新しい設定"Bundler"が使用されています。そのため、ノードの解析に変更しました。    
```json:node_modules/@vue/tsconfig/tsconfig.json
// modify "moduleResolution": "bundler" => "node"
"moduleResolution": "node"
```

## 7. kintone apiリクエストのラッピング
```ts:src/service/kintoneApi.ts
import { KintoneRestAPIClient } from '@kintone/rest-api-client'
import type { GoodListAppRecord } from '@/types/restApiRecords'

export class KintoneApi {
  client: KintoneRestAPIClient
  constructor() {
    this.client = new KintoneRestAPIClient({})
  }

  public async getAllRecords(app: string) {
    return await this.client.record.getAllRecords<GoodListAppRecord>({ app })
  }
}
```

## 8. envファイルにkintoneの設定を追加する
kintoneアプリのapp idを追加します
```sh:.env.development
VITE_APP_ID=xxx
```

## 9. ページの作成
まず、view下の既存のファイルを削除し、以下のファイルを追加します。
view/Home.vue  
view/Contacts.vue  
view/Space.vue  
view/Star.vue  
view/Todo.vue  

#### ページにデモデータを用意する
ポータル画面は、kintoneのデータを取得し、デモの注文データとします。
```vue:src/view/Home.vue
<template>
  <List v-model:loading="loading" :finished="finished" @load="onLoad">
    <Card v-for="(item, index) in list" :key="index" :num="item.num.value" :price="item.price.value"
      :desc="item.desc.value" :title="item.title.value" :thumb="item.thumb.value" />
  </List>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import type { Ref } from 'vue'
import { Card, List } from 'vant'
import { KintoneApi } from '@/service/kintoneApi'

const list: Ref<kintone.types.Fields[]> = ref([])
const loading = ref(false);
const finished = ref(false);

const onLoad = () => {
  const kintoneClient = new KintoneApi()
  kintoneClient.getAllRecords().then((res) => {
    list.value = res
    loading.value = false;
    finished.value = true;
  })
};
</script>
```
他のページでも、デモデータを準備します
例えばContacts.vue
```vue:view/Contacts.vue
<template>
  <div>
    <h1>Contacts</h1>
  </div>
</template>
```

## 10. ルーティングの設定
ここでは、createWebHashHistoryの方法を使用してルーティングを作成します。
createWebHistoryの方法では、リフレッシュ時にバックエンドのルーティングに解析されます。
```ts:src/router/index.ts
import { createRouter, createWebHashHistory } from "vue-router";

const router = createRouter({
  history: createWebHashHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "home",
      component: () => import("../views/Home.vue"),
    },
    {
      path: "/contacts",
      name: "contacts",
      component: () => import("../views/Contacts.vue"),
    },
    {
      path: "/space",
      name: "space",
      component: () => import("../views/Space.vue"),
    },
    {
      path: "/star",
      name: "star",
      component: () => import("../views/Star.vue"),
    },
    {
      path: "/todo",
      name: "todo",
      component: () => import("../views/Todo.vue"),
    },
  ],
});

export default router;
```

## プロジェクトの起動
```sh
npm run dev
```
起動後、kintoneのポータル画面のカスタマイズは自動的にvite_plugin_kintone_dev_module_hack.jsファイルをアップロードします。
![截屏2023-10-10 17.06.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3516963/14365518-83a3-2b08-8883-d9826a64bf39.png)


# 完成イメージ
![68747470733a2f2f73746f726167652e676f6f676c65617069732e636f6d2f7a656e6e2d757365722d75706c6f61642f6539306433343164373630642d32303233303931332e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3516963/745a8f7d-a70e-2e32-308f-7a1c26766006.png)


簡単なモバイルカスタマイズの大枠が出来上がりました。
これは最も簡単な例で、実際には、集中型の状態管理、ページライフサイクルのキャッシュ、データのプルダウンリフレッシュなど、考慮すべき点がたくさんあります。
もしモバイル版のカスタマイズに興味があるなら、こちらのプロジェクトもぜひご覧ください。ただし、当時はまだvue2を使用していました。参考にしてみてください。
https://github.com/kintone-samples/sample-kintone-mobile-customize-CN

# ビルド段階
```sh
npm run build
```
実行が完了すると、ビルド後のコードが自動的にkintoneにアップロードされます。

# サンプルプロジェクト
https://github.com/GuSanle/kintone-mobile-custom-demo/tree/main

# reactでの使用例
このプラグインは、viteを使用してkintoneでreactアプリケーションを構築するのにも適しています。
reactの使用例は以下を参照してください：
https://github.com/GuSanle/vite-plugin-kintone-dev/tree/main/example/react-kintone-vite-demo

# プラグインの原理
プラグインの原理は、JSカスタマイズを通じて、moduleタイプのscriptタグをハックアウトし、main.tsファイルをロードすることです。
ただし、vueとreactの違いもあるため、viteのドキュメントを参考に、コードを調整する必要があります。


# まとめ
viteを使用すると、HMR技術により、コードを変更した後にすぐに変更がページ上で反映され、非常に迅速な開発体験を実現できます。
現在、このプラグインはバージョン2.0.0に更新され、kintoneとViteを完全にサポートするようになりました。以前のイベント登録タイミングの問題を考慮する必要がなくなりました。皆様のご利用をお待ちしております。
もしこのプラグインやこのモバイル例に興味があるなら、ぜひ交流して、スターをつけてください。