コストゼロからはじめよう！React + Vite + Google Apps ScriptでSPAを無料公開

Google Apps Script（GASと呼ばることが多いですが、公式にはApps Scriptと略されます）は、Googleが提供するJavaScriptベースのスクリプティング言語です。このクラウド上で動作する環境を利用することで、GoogleスプレッドシートなどのGoogleアプリと連携したウェブアプリを誰でも無料で開発できます。

https://qiita.com/takatama/items/9b11234ba7c420e1bf7f

この記事では、Apps Scriptの開発ツールであるclaspを用いて、ReactとViteを組み合わせることにより、より洗練されたSingle Page Application（SPA）の開発プロセスを解説します。

- *clasp*: Apps Scriptプロジェクトを効率的に管理するためのコマンドラインツール。ローカルでの開発を可能にし、変更を簡単にGoogle Cloudにプッシュできます。
- *React*: 再利用可能なUIコンポーネントを作成することで、開発プロセスを加速し、保守が容易なアプリケーションを構築できます。
- *Vite*: 開発中の高速なホットリローディングを提供し、最終的なビルドの効率を大幅に向上させるモダンなビルドツールです。

この記事では、clasp + React + Vite + Google Apps Scriptで無料のSPAを公開するまでの手順を具体的に紹介していきます。核となるのは、Reactで作ったフロントエンドをViteのプラグインで一つのindex.htmlにまとめてから、Google Apps Scriptのプロジェクトにpushするプロセスです。Apps Scriptのウェブアプリはページ間のリンク（`<a>`タグによるハイパーリンク）の扱いが得意ではありません。そこでページ間の遷移はReactで実現し、ファイル自体は一つにまとめてしまいます。

開発者はReactの豊富なコンポーネントを再利用しながら、より優れたUIを構築できます。またViteの効率的なビルドシステムのおかげでストレスなく開発できます。ローカル開発環境を準備するのは少々手間がかかりますがデバッグは容易になるので、結果的により高速な開発サイクルを実現できます。

この記事では、`npm create`でViteとReactのボイラープレートを作り、Apps Scriptのウェブアプリとして公開するところまでを紹介します。取り上げる題材は簡単なものですが、これを土台にすることで、より複雑なReactアプリをApp Scriptの環境で開発し公開することも可能になります。ぜひ手を動かしながらお楽しみください。

# 0. 事前準備

PCにNode.jsとclaspをインストールします。

## Node.jsのインストール

WindowsやWSL2にNode.jsをインストールするには、マイクロソフトの記事が参考になります。

https://learn.microsoft.com/ja-jp/windows/dev-environment/javascript/nodejs-on-windows

https://learn.microsoft.com/ja-jp/windows/dev-environment/javascript/nodejs-on-wsl

Macはいろいろな流派がありますが、私はHomebrew + nvmでNode.jsをインストールしています。

https://formulae.brew.sh/formula/nvm

この記事では、次のバージョンを使います。

```console
node -v
v20.9.0
```

## claspのインストール

Node.jsのインストールが完了したら、claspをインストールします。

まず、Google Apps Script APIを`On`にしておきます。

https://script.google.com/home/usersettings

次に、claspをグローバルにインストールし、Googleアカウントでログインしておきます。

https://github.com/google/clasp

```console
npm install -g @google/clasp
clasp login
```

この記事では、次のバージョンを使います。

```console
clasp -v
2.4.2
```

# 1. Reactアプリを作成

Reactアプリを作成します。今回はJavaScriptで作ります[^1]。アプリ名は`hello-apps-script`にします。プロジェクト名（`hello-apps-script`）の後に`--`を記載し忘れないよう注意してください。

```console
npm create vite@latest hello-apps-script -- --template react
```

[^1]: TypeScriptで作ることもできます。

`hello-apps-script`ディレクトリーができるので、開発用サーバーを動かしてReactアプリが動作することを確認します。なお、IDE、例えばVisual Studio Codeを使っている方は、IDEのターミナルから作業すると便利です。

```console
cd hello-world
npm install
npm run dev
```

ターミナルにURLが表示されるので、ウェブブラウザーでアクセスして表示を確認します。

```console
  VITE v5.2.7  ready in 1410 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help
```

![Vite + Reactの画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/1173d33b-0b2c-1bc9-f598-868602458180.png)

`count is 0`ボタンをクリックすると、カウントアップしていきます。

# 2. ビルド環境構築

ViteでApps Scriptにアップロードするファイルをbuildします。

まず必要なパッケージをインストールします。

```console
npm install --save-dev vite-plugin-singlefile vite-plugin-static-copy
```

[vite-plugin-singlefile](https://www.npmjs.com/package/vite-plugin-singlefile)はJavaScriptやCSSを`dist/index.html`にまとめてくれます。

[vite-plugin-static-copy](https://www.npmjs.com/package/vite-plugin-static-copy)は特定のフォルダーにあるファイルを`dist`以下にコピーしてくれます。

次に`vite.config.js`ファイルを編集します。

```javascript:vite.config.js
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import { viteSingleFile } from "vite-plugin-singlefile"
import { viteStaticCopy } from "vite-plugin-static-copy"

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    viteSingleFile(),
    viteStaticCopy({ targets: [{ src: "apps-script/*", dest: "./" }] }),
  ],
})
```

フロントエンドのコードは`src`ディレクトリーに、バックエンドの（Apps Scriptの）コードはこれから作る`apps-script`ディレクトリーに配置する前提です。

ここまで作業したら、正しくbuildできるか試してみます。

```console
npm run build

> hello-apps-script@0.0.0 build
> vite build

vite v5.2.7 building for production...
✓ 34 modules transformed.
rendering chunks (1)...

Inlining: index-BXGOX9Nt.js
Inlining: style-DiwrgTda.css
computing gzip size (0)...[vite-plugin-static-copy] No items to copy.
dist/index.html  150.09 kB │ gzip: 49.62 kB
✓ built in 395ms
```

`dist`ディレクトリーの下に`index.html`ファイルが出力されました。

# 3. Apps Scriptのウェブアプリを作成

claspでApps Scriptのウェブアプリを作成します。スプレッドシートをデータベース代わりに使う事が多いので、`--type sheets`を指定して、スプレッドシートのスクリプトとして作成します。新しいスプレッドシートはGoogleドライブのマイドライブに作成されます。

```console
clasp create --type sheets　--title "HelloAppsScript"
```

:::note info
Could not read API credentials. Are you logged in globally?

というエラーが出た場合、`clasp login`でログインを済ませてから実行してください。
:::

:::note info
User has not enabled the Apps Script API. Enable it by visiting https://script.google.com/home/usersettings then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.

というエラーが出た場合、指示に従って Google Apps Script APIをONにしてから実行してください。
:::

:::note info
Project file (.clasp.json) already exists.

というエラーが出た場合、すでにclasp createでプロジェクトを作成済です。もし、再作成する場合は、ローカルにある.clasp.jsonと、Googleドライブに作った関連ファイルを手動で削除してから再実行してください。
:::

この操作が成功すると、次の2つのファイルが作成されます。

- .clasp.json
  - Apps Scriptのリソースを定義（後ほど修正します）
- appsscript.json
  - Apps Scriptの設定を定義（後ほど移動、修正します）

## .clasp.jsonを修正

`.clasp.json`の`rootDir`は、どのソースコードをGoogle側にpushするかを定義しています。buildした結果をpushしたいので、`dist`ディレクトリーを指定しておきます（`scriptId`と`parentId`はそのままで大丈夫です）。

```json:.clasp.json
  "rootDir": "dist"
```

## apps-scriptディレクトリーを作成

バックエンドのソースコードを置くために、`apps-script`ディレクトリーを作成します。

```console
mkdir apps-script
```

## appsscript.jsonを移動、修正

`appsscript.json`はマニフェストファイルと呼ばる、プロジェクトの設定ファイルです。バックエンドのソースコードなので、`apps-script`ディレクトリーを作り、そこに移動します（buildするタイミングで`dist`ディレクトリーにコピーされます）。

```console
mv appsscript.json apps-script
```

また、このApps Scriptはウェブアプリとして公開するため、`appsscript.json`を次のように編集しておきます。`timeZone`を`Asia/Tokyo`に変更し、`webapp`を追加しています。

```json:apps-script/appsscript.json
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "webapp":  {
    "access": "MYSELF",
    "executeAs": "USER_DEPLOYING"
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}
```

## apps-script/Code.jsを追加

バックエンドのコードとして、`apps-script`ディレクトリーの下に`Code.js`を追加します。ウェブアプリにアクセスがあると、index.htmlを返却するために、doGet()を実装します。

```javascript:apps-script/Code.js
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index.html")
    .addMetaTag("viewport", "width=device-width, initial-scale=1")
    .setTitle("React Router Tutorial");
}
```

## buildしてGoogle側へpush

ここまでに、次のような作業を実施してきました。

```console
.
├── .clasp.json （rootDirをdistに修正）
└── apps-script
    ├── Code.js (doGetを実装）
    └── appsscript.json (webappを追加）
```

buildして、distディレクトリー以下にGoogleへpushするファイル群を作り、pushします。マニフェストファイルを修正しているため、更新してもよいか聞かれるので、`y`と入力します。

```console
npm run build && clasp push

? Manifest file has been updated. Do you want to push and overwrite? Yes
└─ dist/appsscript.json
└─ dist/Code.js
└─ dist/index.html
Pushed 3 files.
```

Google側に正しくpushできたかどうかは、スクリプトエディターで確認できます。なお、スクリプトエディター側でソースコードを修正しないようご注意ください（ローカルには反映されません。clasp pullをする必要があります）。

```console
clasp open
```

![Google Apps Scriptのスクリプトエディター](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/efe45837-2efb-141b-4ac7-61893f9d8fff.png)

# 4. ウェブアプリの確認と修正

Google側にpushしたウェブアプリが正しく動作するか、ウェブブラウザーで確認します。`--webapp`オプションをつけて`clasp open`を実行します。どのデプロイメントを確認するか聞かれますが`@HEAD`を選択してください。

```console
clasp open --webapp
? Open which deployment?                               @HEAD - 
AKfycbzLwLPOUK5pChztlBpdGeQDpoSDcAlP-WsJm1els15I
Opening web application: AKfycbzLwLPOUK5pChztlBpdGeQDpoSDcAlP-WsJm1els15I
```

`@HEAD`デプロイメントは、Google側にpushしたソースコードが即時に反映される開発用のデプロイです。ウェブブラウザーで開いたURLの末尾が`/dev`になっています。このURLには、Apps Scriptの編集権限を持っている人しかアクセスできないことに注意してください。

さて、ウェブアプリを確認してみると、Viteのロゴが表示されていません。一方、Reactのロゴは正しく表示されています。

![Google Apps Scriptで公開したReactアプリ（Viteのロゴが表示されない）](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/ae87de92-5465-05d4-6873-2780ec2f419c.png)

ソースコードを確認してみると、それぞれのロゴデータの置き場所が異なるようです。

```console
.
├── public
│   └── vite.svg
└── src
    ├── assets
    │   └── react.svg
    ├── App.jsx
```

```jsx:App.jsx
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "vite.svg";
```

今回は、正しく表示されているReactのロゴに合わせて、Viteのロゴを移動してみます。

```console
mv public/vite.svg src/assets
```

```diff:App.jsx
import { useState } from "react";
import reactLogo from "./assets/react.svg";
- import viteLogo from "vite.svg";
+ import viteLogo from "./assets/vite.svg";
```

ここまで作業できたら、まずローカル環境で正しく動作しているか確認しましょう。

```console
npm run dev
```

正しく動作していれば、buildとpushをして、@HEADデプロイを確認します。

```console
npm run build && clasp push
clasp open --webapp
（@HEADを選びます）
```

![スクリーンショット 2024-03-30 16.33.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6760/2f53dd9b-f89e-a2c5-65bc-4cb58b8f2173.png)

今度は正しく動作しているようです。

# 5. ウェブアプリをデプロイして他の人が使えるようにする

ここまでは開発用の`@HEAD`デプロイメントで動作を確認してきました。このデプロイはApps Scriptの編集権限を持っている開発者しかアクセスできません。

ウェブアプリを自分以外にも使ってもらうためには、新しくデプロイする必要があります。具体的にはマニフェストファイル`appsscript.json`、`webapp`、`access`に`MYSELF`以外（`DOMAIN`、`ANYONE`、`ANYONE_ANONYMOUS`）を設定する場合です。

https://developers.google.com/apps-script/manifest/web-app-api-executable?hl=ja#webapp

なお、ウェブアプリのURLは新しくデプロイをすると変更されます。URLは変更せずに、ウェブアプリの動作を変える場合には、deployment IDを指定した再デプロイを実行する必要があります。

とはいえ、最初の一度は新しくデプロイする必要があります。

```console
clasp deploy

Created version 1.
- AKfycbwQgENC3rb2Upr6crhbCJPcfMRbP_Mk9tyw-9kgYSyNrxGrm18ZUJUTNfK7WBqR9g6DmA @1.
```

Created version 1.の次の行、@1 までの文字列が deployment IDです。再デプロイに必要となります。`clasp deployments`を実行すると、再表示できます。

```console
clasp deployements

2 Deployments.
- AKfycbzLwLPOUK5pChztlBpdGeQDpoSDcAlP-WsJm1els15I @HEAD 
- AKfycbwQgENC3rb2Upr6crhbCJPcfMRbP_Mk9tyw-9kgYSyNrxGrm18ZUJUTNfK7WBqR9g6DmA @1 
```

自分以外の人も使えるウェブアプリのURLは、末尾が`/exec`になっています。`clasp open --webapp`で、新しくデプロイしたウェブアプリを選択し、ウェブブラウザーのURLを自分以外の人に動作の確認をお願いしてください。

## ウェブアプリの修正が完了したら再デプロイする

ウェブアプリの修正をし、`@HEAD`デプロイでの動作が完了したら、再デプロイが必要になります。再デプロイするまでは、他の人のウェブアプリに動作は反映されません。

他の人に「修正したって聞いたけど、前と動作が同じだよ」と連絡をもらったら、再デプロイし忘れていないかを確認しましょう。

再デプロイはdeployment IDを指定する必要があります。

```console
clasp deploy --deploymentId AKfycbwQgENC3rb2Upr6crhbCJPcfMRbP_Mk9tyw-9kgYSyNrxGrm18ZUJUTNfK7WBqR9g6DmA

Created version 2.
- AKfycbwQgENC3rb2Upr6crhbCJPcfMRbP_Mk9tyw-9kgYSyNrxGrm18ZUJUTNfK7WBqR9g6DmA @2.
```
再デプロイすると、@の後のバージョン番号が増えていきます。

## 開発用・本番用のデプロイまとめ

開発用と本番用でデプロイが分かれているのは、慣れてしまえば便利ですが、最初は混乱しがちです。特徴を以下にまとめておきます。

| | 開発用デプロイ | 本番用デプロイ |
|---|---|---|
| URL | 末尾が`/dev` | 末尾が `/exec` |
| アクセスできるのは | 自分だけ[^2] | 自分以外も[^3] |
| 修正後の動作確認 | pushだけでOK | 再デプロイが必要 |

[^2]: 正しくは、Apps Scriptの編集権限を持っている人がアクセスできる
[^3]: 正しくは、デプロイの公開設定による（自分だけ / Googleアカウントを持っている人 / 全員）

なお再デプロイにはdeployment IDが必要です。再デプロイではなく、新しくデプロイするとURLが変更になるのでご注意ください。

# 参考にした記事

次の記事を大いに参考にさせていただきました。とてもわかりやすい記事をありがとうございます。

https://engineer.retty.me/entry/2022/12/22/150035

題材は同じなのですが、この記事とは細かい相違点があります。
- ビルドした成果物を`dist`に格納し、gitの管理対象外とする 
  - 成果物は他のソースコードからいつでも再生成できます
  - 成果物はサイズも大きくなりがちです
  - 上記の理由から、`dist`はgitでは管理していません
- 開発用デプロイを使った動作確認
  - pushするだけで（デプロイ不要で）動作が変わる開発用デプロイ`@HEAD`の使い方を紹介しています