②GAS(GoogleAppsScript)とVueを使いWebアプリ(タスク管理アプリ)を作ってみる【CRUD】
GAS(GoogleAppsScript)とVueを使いWebアプリ(タスク管理アプリ)を作ってみる
記事構成 ①ブラウザ画面を表示する ②CRUDしてみる←イマココ
完成画像↓
概要
「GASでスプレットシートを使ってデータベースみたいなことできない?」という命を受け調査。 とりあえず、「みんな大好きタスク管理アプリ」を作ってみました。 VueとBootstrap-VueでCRUDを構築していきます。
全ソースコード
// server.gs
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
function doGet() {
var template = 'index';
const htmlOutput = HtmlService.createTemplateFromFile(template).evaluate();
htmlOutput.setTitle('タスク管理アプリ【GAS】');
return htmlOutput;
}
function doPost(e) {
// パラメータから実行関数を分岐する。
const parameter = e.parameter.parameter;
switch (parameter){
case 'getAll':
return doGetAll();
break;
case 'create':
return doCreate(e);
break;
case 'update':
return doUpdate(e);
break;
case 'done':
return changeDone(e);
break;
case 'delete':
return doDelete(e);
break;
default:
doGet();
}
}
// タスクを全取得してくる関数
function doGetAll() {
try {
let st = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('contents');
let lastrow = st.getLastRow();
// セルの範囲を取得する
const range = st.getRange(2, 1, lastrow - 1, 3);
// セル範囲の値を2次元配列で取得する
const todos = range.getValues();
return makeResponse('200', 'getAll', todos);
} catch (err) {
logError(err);
}
}
// タスクをスプレットシートに書き込む関数
function doCreate(e) {
try {
const todo = e.parameter.todo;
let st = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('contents');
let lastrow = st.getLastRow();
let lastId = st.getRange(lastrow,1).getValue();
if(lastId && typeof lastId === 'number') {
lastId++;
} else {
lastId = 1;
}
st.getRange(lastrow+1,1).setValue(lastId);
st.getRange(lastrow+1,2).setValue(todo);
st.getRange(lastrow+1,3).setValue(false);
return makeResponse('200', 'create');
} catch (err) {
logError(err);
}
}
// タスクの状態を更新する関数
function changeDone(e) {
try {
const id = e.parameter.id;
// logError('id:' + id);
let st = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('contents');
let lastrow = st.getLastRow();;
// セルの範囲を取得する
const range = st.getRange(1, 1, lastrow - 1, 3);
// セル範囲の値を2次元配列で取得する
const todos = range.getValues();
let row = todos.length + 1;
for(let i = 0; i<todos.length; i++){
// logError('todos:' + todos[i][0]);
if(todos[i][0] == id){
row = i + 1;
break;
}
}
// logError('row:' + row)
if(row == 0) {
return makeResponse('400', 'doneError');
}
const done = st.getRange(row,3).getValue() ? false : true;
st.getRange(row,3).setValue(done);
return makeResponse('200', 'done');
} catch (err) {
logError(err);
}
}
// タスクを更新する関数
function doUpdate(e) {
try {
const id = e.parameter.id;
const todo = e.parameter.todo;
let st = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('contents');
let lastrow = st.getLastRow();;
// セルの範囲を取得する
const range = st.getRange(1, 1, lastrow - 1, 3);
// セル範囲の値を2次元配列で取得する
const todos = range.getValues();
let row = todos.length + 1;
for(let i = 0; i<todos.length; i++){
if(todos[i][0] == id){
row = i + 1;
break;
}
}
if(row == 0) {
return makeResponse('400', 'updateError');
}
st.getRange(row,2).setValue(todo);
return makeResponse('200', 'update');
} catch (err) {
logError(err);
}
}
// タスクを削除する関数
function doDelete(e) {
try {
const id = e.parameter.id;
let st = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('contents');
let lastrow = st.getLastRow();;
// セルの範囲を取得する
const range = st.getRange(1, 1, lastrow - 1, 3);
// セル範囲の値を2次元配列で取得する
const todos = range.getValues();
let row = todos.length + 1;
for(let i = 0; i<todos.length; i++){
if(todos[i][0] == id){
row = i + 1;
break;
}
}
if(row == 0) {
return makeResponse('400', 'deleteError');
}
st.deleteRow(row);
return makeResponse('200', 'delete');
} catch (err) {
logError(err);
}
}
// フロントへ返却するレスポンスを構築する関数
function makeResponse(status, message, payload = null) {
const data = {
status: status,
message: message,
data: payload,
}
const output = ContentService.createTextOutput(JSON.stringify(data));
output.setMimeType(ContentService.MimeType.JSON);
return output;
}
// エラーシートにログを残す関数
function logError(err) {
const st = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('errors');
const lastrow = st.getLastRow();
st.getRange(lastrow+1,1).setValue(err);
}
// index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base target="_top">
<!-- <script src="https://cdn.jsdelivr.net/npm/vue"></script> -->
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- Load polyfills to support older browsers -->
<!-- <script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script> -->
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.min.js"></script>
<!-- Load the following for BootstrapVueIcons support -->
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
<?!= include('css'); ?>
</head>
<body>
<br>
<div id="app">
<b-container class="text-center mt-5" style="width:100%">
<b-modal id="modal-edit" title="タスク編集" @ok="doUpdate()">
<p class="my-4">タスクID:{{dialogId}}</p>
<div>
<b-form-input
v-model="editTodo"
placeholder="やること"
></b-form-input>
</div>
</b-modal>
<b-modal id="modal-delete" title="タスク削除" @ok="doDelete()">
<p class="my-4">ID:{{dialogId}}のタスクを削除しますか?</p>
</b-modal>
<b-alert
class="fixed-top mx-auto mt-2"
style="width:90%"
:show="dismissCountDown"
:variant="alertColor"
@dismissed="dismissCountDown=0"
@dismiss-count-down="countDownChanged"
fade
>
{{alertText}}
</b-alert>
<b-form @submit="onSubmit">
<b-row>
<b-container class="mb-2">
<b-row>
<b-col cols="10">
<b-form-input
v-model="newTodo"
placeholder="やること"
></b-form-input>
</b-col>
<b-col cols="2">
<b-button
pill
variant="outline-success"
@click="onSubmit"
:disabled="isDisabled"
>タスク登録</b-button>
</b-col>
</b-row>
</b-container>
<b-alert show class="text-center">◆タスク一覧◆</b-alert>
<b-col>
<b-list-group>
<b-list-group-item class="mb-1">
<div class="d-flex justify-content-between">
<div class="d-flex align-items-center mr-1 h5">
ID
</div>
<div class="d-flex align-items-center" style="max-width: 270px">
タスク名
</div>
<div class="d-flex align-items-center justify-content-center" style="width: 160px">
操作ボタン
</div>
</div>
</b-list-group-item>
<b-list-group-item v-for="todo in todos" :key="todo.id" class="mb-1">
<div class="d-flex justify-content-between">
<div class="d-flex align-items-center mr-1 h5">
{{todo.id}}
</div>
<div class="d-flex align-items-center wrap_text left px-2" :class="{ done: todo.done }" style="max-width: 270px">
{{todo.todo}}
</div>
<div class="d-flex align-items-center no_padding" style="width: 160px">
<div>
<b-button
pill
variant="outline-warning"
size="sm"
@click="changeDone(todo.id)"
:disabled="isDisabled"
>完了</b-button>
<b-button
v-b-modal.modal-edit
pill
variant="outline-info"
size="sm"
@click="onEdit(todo.id, todo.todo)"
:disabled="isDisabled"
>編集</b-button>
<b-button
v-b-modal.modal-delete
pill
variant="outline-danger"
size="sm"
@click="onDelete(todo.id)"
:disabled="isDisabled"
>削除</b-button>
</div>
</div>
</div>
</b-list-group-item>
</b-list-group>
</b-col>
</b-row>
</b-form>
</b-container>
</div>
<?!= include('js'); ?>
</body>
</html>
// css.html
<style>
.done {
text-decoration: line-through;
}
.wrap_text {
word-break: break-all;
overflow-wrap: break-word;
}
.left {
text-align: left;
}
.right {
text-align: right;
}
.no_padding {
padding: 0;
}
</style>
// js.html
<script>
new Vue({
el: "#app",
data: {
url: 'デプロイURL', // ここにデプロイしたURLを設定する
todos: [], // スプレットシートに保存されたタスクが格納される
newTodo: "", // 新規タスク作成するための変数
editTodo: "", // タスクを更新する時に格納するための変数
dialogId: "",
isDisabled: false,
alertColor: "info",
alertText: "",
dismissSecs: 5,
dismissCountDown: 0
},
methods: {
// スプレットシートに保存されたタスクを取得する関数
async getAll() {
console.log('getAll')
// パラメータにgetAllを仕込んでバックエンドで分岐させる
const data = {
'parameter': 'getAll',
}
const params = new URLSearchParams(data)
// axiosでポストする
await axios.post(this.url, params, {
headers: {'content-type': 'application/x-www-form-urlencoded'}
})
.then((res) => {
console.log(res.data.data)
const todos = res.data.data
for (let i = 0; i < todos.length; i++) {
const todo = {
id: todos[i][0],
todo: todos[i][1],
done: todos[i][2],
}
this.todos.push(todo);
}
})
.catch((err) => {
console.log(err)
});
},
// タスク登録ボタンを押したときの処理。新規タスクを作成する
async onSubmit(){
console.log('submit')
this.isDisabled = true;
// パラメータにcreateを仕込んでバックエンドで分岐させる
const parameter = {
'parameter': 'create',
'todo': this.newTodo,
}
const params = new URLSearchParams(parameter)
await axios.post(this.url, params, {
headers: {'content-type': 'application/x-www-form-urlencoded'}
})
.then((res) => {
console.log(res);
this.resetTodos();
this.getAll();
this.showAlert('success', 'タスクを追加しました。');
})
.catch((err) => {
console.log(err)
});
this.newTodo = "";
this.isDisabled = false;
},
// タスクの状態を更新する関数
async changeDone(id){
console.log('changeDone')
this.isDisabled = true;
// パラメータにdoneを仕込んでバックエンドで分岐させる
const parameter = {
'parameter': 'done',
'id': id,
}
const params = new URLSearchParams(parameter)
await axios.post(this.url, params, {
headers: {'content-type': 'application/x-www-form-urlencoded'}
})
.then((res) => {
console.log(res);
this.resetTodos();
this.getAll();
this.showAlert('warning', 'タスクの状態を更新しました。');
})
.catch((err) => {
console.log(err)
});
this.newTodo = "";
this.isDisabled = false;
},
// タスクを更新する関数
async doUpdate(){
console.log('doUpdate')
this.isDisabled = true;
// パラメータにupdateを仕込んでバックエンドで分岐させる
const parameter = {
'parameter': 'update',
'id': this.dialogId,
'todo': this.editTodo,
}
const params = new URLSearchParams(parameter)
await axios.post(this.url, params, {
headers: {'content-type': 'application/x-www-form-urlencoded'}
})
.then((res) => {
console.log(res);
this.resetTodos();
this.getAll();
this.showAlert('info', 'タスクを更新しました。');
})
.catch((err) => {
console.log(err)
});
this.editTodo = "";
this.isDisabled = false;
},
// タスクを削除する関数
async doDelete(){
console.log('doDelete')
this.isDisabled = true;
// パラメータにdeleteを仕込んでバックエンドで分岐させる
const parameter = {
'parameter': 'delete',
'id': this.dialogId,
}
const params = new URLSearchParams(parameter)
await axios.post(this.url, params, {
headers: {'content-type': 'application/x-www-form-urlencoded'}
})
.then((res) => {
console.log(res);
this.resetTodos();
this.getAll();
this.showAlert('danger', 'タスクを削除しました。');
})
.catch((err) => {
console.log(err)
});
this.dialogId = "";
this.isDisabled = false;
},
// 編集ボタンを押したときの処理
onEdit(id, todo) {
this.dialogId = id;
this.editTodo = todo;
},
// 削除ボタンを押したときの処理
onDelete(id) {
this.dialogId = id;
},
// アラートの表示時間カウントダウン
countDownChanged(dismissCountDown) {
this.dismissCountDown = dismissCountDown
},
// アラートの表示
showAlert(color, text) {
this.alertText = "";
this.alertText = text;
this.alertColor = color;
this.dismissCountDown = this.dismissSecs
},
// タスクリセット
resetTodos() {
this.todos = []
}
},
mounted(){
// 画面が呼び出されたらタスクをバックエンドから取得するgetAll関数を実行
this.getAll();
}
})
</script>
スプレットシートはこんな感じです。 「contentsシート」と「errorsシート」を作成します。
デプロイで認証を問われるので少しやっかいですが、許可させましょう。
デプロイしたら動かしてみましょう。
まとめ&感想
ちょっと処理の遅さが気になります。 試してはみましたが、変則的ですし処理速度も遅いのであまり積極的にGASでWebアプリを作ることはないかもしれません。 とはいえスプレッドシートなどのGoogleツールとの連携させた自動化システムをちょっと作るのに最適なGASは今後も勉強していこうと思います。 次はHerokuなどにデプロイする記事を書けたらな、と思っています。
記事構成 ①ブラウザ画面を表示する ②CRUDしてみる←イマココ
訓志
笑わんといてw
タカモリ
GASって聞いたことくらいしかないので、あまりわかってないのですが。。 訓志さんのコード見れるのが楽しい!笑?