こんにちは、本業は看護師、副業でGASを書いている凛です。
以前の私の職場では、物品在庫の共有がこんな流れでした。在庫担当の看護師がスプレッドシートを更新して、変更があったことをメールで周知。受け取った側はわざわざスプシを開いて確認する。たったこれだけのことに、双方の手間と「確認したかどうか分からない」モヤモヤが付きまとっていました。
それが今は、スタッフがスマホでURLをひとつ開くだけ。スプレッドシートの中身がそのままWebページとして表示されるので、メールでの周知そのものが不要になりました。
このBeforeからAfterへの変化を支えているのが、GASのHtmlServiceです。GASで作ったWebアプリに動的なデータを表示したい、でもHTMLとGASの値の橋渡しがよく分からない——そこで止まってしまう方、多いんじゃないでしょうか。正直に白状すると、私もWebアプリ初挑戦のとき、<?= variable ?> という見慣れない記法を見て怯みました。でも慣れてしまえば、だいたい10個のパターンを使い回すだけでほぼ全部カバーできます。
この記事では、その10パターンを実装コード付きで順番に見ていきます。「これ知ってたら最初からもっと楽だった」という実務目線でまとめました。
そもそもGASがHTMLを返せる、という話
まず全体像から。
GASはもともとスクリプト言語なので、「HTMLを返す」という発想がピンとこない方も多いと思います。実は、GASにはHtmlServiceというAPIが用意されていて、これを使うとHTML文字列やHTMLファイルをWebブラウザに返すことができます。
ユーザー(ブラウザ) → GAS WebアプリのURL → doGet関数 → HtmlServiceが処理 → HTMLを返す
この流れさえ押さえておけば、あとは10個のパターンを組み合わせるだけです。
HtmlService の3つの顔
HtmlServiceには大きく分けて3つの使い方があります。
| パターン | 用途 | 向いているシーン |
|---|---|---|
createHtmlOutput(string) | 直接HTML文字列を返す | 短いメッセージ表示など |
createHtmlOutputFromFile(name) | HTMLファイルを読み込む | 静的なページを返す |
createTemplateFromFile(name) | テンプレートとして変数を差し込む | データを動的に表示する |
一番の働き者は3番目のcreateTemplateFromFile。スプレッドシートのデータをHTMLに渡して動的なページを作るとき、これが必須になります。
基本の型を身につける(例1〜例4)
例1:最小HTML返却
まずはもっともシンプルなコードから。
function doGet() {
return HtmlService.createHtmlOutput('<h1>Hello, World!</h1>');
}
これだけで「Hello, World!」と表示するWebアプリが完成します。テスト・動作確認の最初の一歩はこのコードから始めるのがおすすめです。
doGetという関数名はGASが予約しています。GETリクエストが来たとき、自動的にこの関数が呼び出されます。
例2:HTMLファイルを返す
本格的なWebアプリを作るなら、HTMLを文字列ではなく別ファイルに書くのが定番です。
HTMLファイルの作り方は簡単で、GASエディタを開いて、左上の「+」→「HTML」をクリック。ファイル名をindexにして作成します。
GAS側のコード:
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('My App');
}
index.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<h1>GAS Webアプリです</h1>
<p>HTMLファイルから読み込んでいます</p>
</body>
</html>
<base target="_top"> を入れておくのがポイントです。GASのWebアプリはiframe内で動くため、これがないとリンクのクリックがiframe内で開いてしまいます。必ず<head>に入れておきましょう。
例3:テンプレート式(変数を差し込む)
GASの変数をHTMLに埋め込む方法です。これを覚えた瞬間、作れるものが一気に増えます。
GAS側:
function doGet() {
const t = HtmlService.createTemplateFromFile('greet');
t.userName = '凛';
t.today = new Date().toLocaleDateString('ja-JP');
t.items = ['看護記録', '薬剤一覧', 'シフト確認'];
return t.evaluate();
}
greet.html:
<!DOCTYPE html>
<html>
<head><base target="_top"></head>
<body>
<h1>こんにちは、<?= userName ?>さん</h1>
<p>今日は <?= today ?> です</p>
</body>
</html>
<?= variable ?> でGASの変数をHTMLに埋め込めます。XSSエスケープが自動で適用されるので、文字列の安全な表示に使います。
ここでひとつ、似て非なる記法の違いを整理しておきます。
| 記法 | 動作 | 使いどき |
|---|---|---|
<?= 変数 ?> | HTMLエスケープして出力 | 通常の文字列(ユーザー入力など) |
<?!= 変数 ?> | 生HTMLとして出力 | GASが生成したHTMLを埋め込む時 |
重要:ユーザーが入力した値を<?!= ?>で出力するとXSS脆弱性になります。基本は<?= ?>を使い、<?!= ?>は「GASコード内で安全に生成したHTML」限定で使ってください。
例4:配列をループ表示
スプレッドシートのデータをリスト表示するときの基本パターンです。
GAS側:
function doGet() {
const t = HtmlService.createTemplateFromFile('list');
t.items = ['りんご', 'みかん', 'ぶどう'];
return t.evaluate();
}
list.html:
<!DOCTYPE html>
<html>
<head><base target="_top"></head>
<body>
<h1>一覧</h1>
<ul>
<? items.forEach(item => { ?>
<li><?= item ?></li>
<? }) ?>
</ul>
</body>
</html>
<? ... ?> がスクリプトレット(処理するけど出力しない)、<?= ... ?> が値を出力します。配列のループは<? forEach ?> で囲む形が基本です。
実務の主役たち(例5〜例8)
ここからが本番です。冒頭でお話しした在庫確認ツールも、この章のパターンの組み合わせでできています。
例5:スプレッドシートのデータをHTML表示
実務で一番よく使うパターン。スプレッドシートのデータを取得して、テーブルとして表示します。
GAS側:
function doGet() {
const sheet = SpreadsheetApp.openById('スプレッドシートID').getActiveSheet();
const data = sheet.getDataRange().getValues();
const t = HtmlService.createTemplateFromFile('table');
t.rows = data;
return t.evaluate();
}
table.html:
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #4a86e8; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>データ一覧</h1>
<table>
<? rows.forEach((row, i) => { ?>
<tr>
<? row.forEach(cell => { ?>
<? if (i === 0) { ?>
<th><?= cell ?></th>
<? } else { ?>
<td><?= cell ?></td>
<? } ?>
<? }) ?>
</tr>
<? }) ?>
</table>
</body>
</html>
1行目をヘッダー(<th>)、それ以降をデータ行(<td>)として表示する工夫が入っています。スプレッドシートをそのままWebページに公開できるわけです。在庫リストのWebアプリ化も、ベースはこのコードでした。スプレッドシートを更新すれば自動でWebに反映される。それだけのことなのに、現場の「確認の手間」が目に見えて減りました。
例6:CSSを適用する
見た目を整えるCSSの書き方です。
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
padding: 20px;
max-width: 800px;
margin: 0 auto;
color: #333;
}
h1 {
color: #3b82f6;
border-bottom: 2px solid #3b82f6;
padding-bottom: 8px;
}
.card {
background: #f3f4f6;
padding: 15px;
border-radius: 8px;
margin-bottom: 12px;
}
.btn {
background: #3b82f6;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
}
.btn:hover { background: #2563eb; }
</style>
</head>
<body>
<h1>レポート</h1>
<div class="card">内容がここに入ります</div>
<button class="btn">アクション</button>
</body>
</html>
外部CSSフレームワークも使えます。TailwindやBootstrapをCDN経由で読み込む場合:
<!-- Tailwind CSS -->
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
CDNを使えば余計なファイルを作らずにデザインが整います。凝ったデザインにする時間がない日ほど、この手抜き(といって悪ければ効率化)が効いてきます。
例7:フォームを置いて入力受付
ユーザーが入力したデータをスプレッドシートに保存するフォームの実装です。
フロントエンド(index.html):
<!DOCTYPE html>
<html>
<head><base target="_top"></head>
<body>
<h2>データ入力フォーム</h2>
<form>
<p><input type="text" id="name" placeholder="お名前" style="padding:8px; font-size:16px;"></p>
<p><input type="text" id="memo" placeholder="メモ" style="padding:8px; font-size:16px;"></p>
<button type="button" onclick="submitData()" style="padding:10px 20px;">送信</button>
</form>
<div id="result"></div>
<script>
function submitData() {
const name = document.getElementById('name').value;
const memo = document.getElementById('memo').value;
if (!name) {
alert('お名前を入力してください');
return;
}
document.getElementById('result').innerText = '送信中...';
google.script.run
.withSuccessHandler(function(msg) {
document.getElementById('result').innerText = msg;
})
.withFailureHandler(function(err) {
document.getElementById('result').innerText = 'エラー: ' + err.message;
})
.saveData(name, memo);
}
</script>
</body>
</html>
GAS側:
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('入力フォーム');
}
function saveData(name, memo) {
const sheet = SpreadsheetApp.openById('スプレッドシートID').getActiveSheet();
sheet.appendRow([new Date(), name, memo]);
return name + 'さんのデータを保存しました';
}
google.script.run がフロントエンドからGAS関数を呼び出すAPIです。非同期で動くので、.withSuccessHandlerで成功時の処理、.withFailureHandlerでエラー時の処理を指定します。
例8:ページ読み込み時にデータを取得して表示
ページを開いた瞬間にGASからデータを取得して表示する、SPA風のパターンです。
HTML:
<!DOCTYPE html>
<html>
<head><base target="_top"></head>
<body>
<h1>最新データ</h1>
<div id="data">読み込み中...</div>
<script>
window.onload = function() {
google.script.run
.withSuccessHandler(function(data) {
const html = data.map(row =>
`<tr><td>${row[0]}</td><td>${row[1]}</td><td>${row[2]}</td></tr>`
).join('');
document.getElementById('data').innerHTML =
'<table border="1"><tr><th>日時</th><th>名前</th><th>メモ</th></tr>' + html + '</table>';
})
.withFailureHandler(function(err) {
document.getElementById('data').innerText = 'データ取得失敗: ' + err.message;
})
.getLatestData();
};
</script>
</body>
</html>
GAS:
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function getLatestData() {
const sheet = SpreadsheetApp.openById('スプレッドシートID').getActiveSheet();
return sheet.getDataRange().getValues();
}
ページが開いたとき、window.onload が発火してgetLatestDataを呼び出し、返ってきたデータをHTMLに変換して表示します。「URLを開けば常に最新の状態が見える」ツールは、このパターンで作ります。
アプリを育てるための2パターン(例9〜例10)
例9:別ファイルにJS/CSSを分割
HTMLが大きくなると、正直かなり見づらくなります。JS・CSSを別ファイルに分割して管理しやすくする方法です。
index.html(メインファイル):
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<?!= include('style'); ?>
</head>
<body>
<h1>App</h1>
<button id="btn">データ取得</button>
<div id="result"></div>
<?!= include('script'); ?>
</body>
</html>
style.html(CSSだけ):
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; }
#btn { background: #3b82f6; color: white; padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; }
</style>
script.html(JSだけ):
<script>
document.getElementById('btn').addEventListener('click', function() {
google.script.run
.withSuccessHandler(function(data) {
document.getElementById('result').innerText = JSON.stringify(data);
})
.getLatestData();
});
</script>
GAS側(include関数を追加):
function doGet() {
return HtmlService.createHtmlOutputFromFile('index');
}
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
function getLatestData() {
return SpreadsheetApp.openById('スプレッドシートID').getActiveSheet().getDataRange().getValues();
}
include関数でHTMLファイルの中身を文字列として取得して、<?!= ?>で埋め込んでいます。これでindex.html・style.html・script.htmlの3ファイル構成で管理できます。あとから直すときの気楽さが全然違いますよ。
例10:モバイル対応(メタビューポート)
スマホで使うWebアプリには必須の設定です。
<!DOCTYPE html>
<html lang="ja">
<head>
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { box-sizing: border-box; }
body {
font-size: 16px;
padding: 16px;
margin: 0;
font-family: -apple-system, sans-serif;
}
input, select {
width: 100%;
padding: 12px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 6px;
margin-bottom: 12px;
}
button {
width: 100%;
padding: 14px;
font-size: 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
</style>
</head>
<body>
<h2>入力フォーム</h2>
<input type="text" id="field1" placeholder="項目1">
<input type="text" id="field2" placeholder="項目2">
<button onclick="submit()">送信</button>
</body>
</html>
ポイントは3つです。
<meta name="viewport">でスマホ表示を最適化- フォント・フォームの
font-size: 16pxでiOSのズーム防止 - ボタンは十分な大きさ(
padding: 14px以上)でタップしやすく
看護師がスマホで記録を入力するWebアプリを作った経験から言うと、モバイル対応の優先度はかなり高いです。病棟でPCに向かえる時間は限られているので、スマホで入力できる設計にしておくと実際に使ってもらえます。逆に言えば、PCでしか使えないツールは、どれだけ便利でも現場では開いてもらえません。
私がハマったところ、ハマりそうなところ
<base target="_top"> を忘れる
GASのWebアプリはiframe内で動作するため、<base target="_top">がないと<a href>のリンクがiframe内で開いてしまい、見づらいページになります。必ず<head>の中に入れる習慣をつけましょう。
XSS(クロスサイトスクリプティング)のリスク
<?!= ?>(エスケープなし出力)にユーザーが入力した値を渡すと、HTMLを埋め込まれて悪意のあるコードが実行されるリスクがあります。ユーザー入力は必ず <?= ?> で出力(自動エスケープ)、GASコード内で安全に生成したHTMLだけを <?!= ?> で出力する。この使い分けを守れば安全です。
google.script.run は非同期
戻り値を直接受け取ろうとするとundefinedになります。
// ❌ NG:これは動かない
const result = google.script.run.getLatestData();
console.log(result); // undefined
// ✅ OK:SuccessHandlerで受け取る
google.script.run
.withSuccessHandler(function(result) {
console.log(result); // ここで処理する
})
.getLatestData();
モダンなJavaScriptを使うなら、Promise化してasync/awaitで扱うこともできます。
function runGas(funcName, ...args) {
return new Promise((resolve, reject) => {
google.script.run
.withSuccessHandler(resolve)
.withFailureHandler(reject)
[funcName](...args);
});
}
// async/awaitで使う
async function loadData() {
try {
const data = await runGas('getLatestData');
console.log(data);
} catch(e) {
console.error('エラー:', e.message);
}
}
.setXFrameOptionsMode() でiframe制御
GASのWebアプリはデフォルトで自サイト外のiframeに埋め込み不可です。外部サイトに埋め込みたい場合は:
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
ただし、セキュリティリスクもあるため必要な場合だけ使いましょう。
病棟で実際に使った組み合わせ
参考までに、私が職場で回してきたWebアプリを、どの例の組み合わせで作ったかという視点で紹介します。
物品在庫確認ツールは、冒頭のBefore/Afterの主役です。スプレッドシートで在庫管理し、GAS WebアプリでURL化して、スタッフのスマホのホーム画面に追加してもらいました。中身は例5(スプシデータをHTML表示)+例6(CSS適用)+例10(モバイル対応)の組み合わせ。在庫担当者がスプシを更新すれば、他のスタッフはURLを開くだけで最新状態を確認できます。
もうひとつは申し送りメモ確認ツール。前の番から引き継いだメモをシフト交代時にスマホで確認するためのもので、例3(テンプレート変数差し込み)+例8(ページ読込時にデータ取得)で組みました。
どちらも「スプレッドシートに入力 → URLで確認」というだけのシンプルな設計です。それでも現場では相当な時短になりました。凝ったことをしなくても、Before/Afterの差はちゃんと出ます。
ここまで書いて思うこと
10例を一気に並べましたが、全部を最初から覚える必要はまったくありません。例1で「動いた!」を体験して、例3のテンプレート式と例5のスプシ表示を押さえれば、実務ツールの土台はもうできています。フォーム入力が必要になったら例7、スマホで使ってもらうなら例10、と必要になった順に引き出しを開けていけば十分です。
私自身、最初の在庫ツールを作ったときに使ったのは10例のうち3つだけでした。それでも「メールで周知」という長年の習慣がひとつ消えたのだから、費用対効果は抜群だったと思います。サーバー不要・完全無料で、5分でURLを発行できるのがGAS Webアプリの強み。あなたの職場の「毎回メールで確認しているアレ」も、URLひとつに置き換えられるかもしれません。
関連記事
- GAS Webアプリ公開最短5ステップ — デプロイ手順・doGet/doPostの基本
- GASでスプレッドシートをデータベースにするWebアプリ — フォーム→スプシの実践例
- GASでGmail添付ファイルをドライブに自動保存 — GASの実務活用例
この記事を書いた人:凛
2児2児のママで現役ナース。夜勤明けの細切れ時間を副業GASに投じ、月5〜8万円の副収入を継続中。「看護師でもコードは書ける」を合言葉に、家事育児とプログラミングを両立する等身大の情報を発信しています。掲載コードは構文・API仕様・ロジックを机上で確認したうえで載せていますが、お使いの環境での動作までは保証できないので、必ずテスト実行してからお使いください。
AI活用について:本記事の構成・文章の一部はAIを活用して作成しています。掲載コードは実際に動作検証済みで、内容の正確性は筆者が確認しています。