mitsuのぶろぐ

基本的にはプログラミングの話のつもり。

GASでspreadsheetに追記できるwebアプリを作ってみた。

Webアプリからspreadsheetにデータを送りたかったんですが、大変でした。

TL;DR

簡単にwebappを作るなら

  1. SpreadSheetからgoogle apps scriptを生成する
  2. VueCLI3でプロジェクト作成し、vue.config.jsをいじる
  3. claspインストールし、2で作成したプロジェクトの成果物をpushできるようにする
  4. POSTで書き込みをしようとするのではなく、 google.script.run.○○ で実行する

解説

1. SpreadSheetからgoogle apps scriptを生成する

SpreadSheetからgasを作成します。 この際gasとsheetを分けても問題ないが、呼び出しが少し面倒になるため、個人的にはsheetから生成したほうがいいと思っています。

2. VueCLI3でプロジェクト作成し、vue.config.jsをいじる

vue create gasapp
cd gasapp
npm i -D html-webpack-inline-source-plugin webpack-cdn-plugin

プロジェクトを作成し、以下のvue.config.jsで使うためのモジュールをinstall。 cli.vuejs.org

// vue.config.js
module.exports = {
  chainWebpack: config => {
    // disable prefetch and preload
    config.plugins.delete("prefetch");
    config.plugins.delete("preload");

    // Make js and css files inline into index.html
    config
      .plugin("html-inline-source")
      .use(require("html-webpack-inline-source-plugin"));
    config.plugin("html").tap(args => {
      args[0].inlineSource = "(/css/.+\\.css|/js/.+\\.js)";
      return args;
    });

    // make inline images
    config.module
      .rule("images")
      .use("url-loader")
      .options({});

    // make inline media
    config.module
      .rule("media")
      .use("url-loader")
      .options({});

    // make inline fonts
    config.module
      .rule("fonts")
      .use("url-loader")
      .options({});

    // make inline svg
    config.module
      .rule("svg")
      .uses.delete("file-loader")
      .end()
      .use("url-loader")
      .loader("url-loader")
      .options({});

    // Get npm modules from CDN
    config.plugin("webpack-cdn").use(require("webpack-cdn-plugin"), [
      {
        modules: [
          {
            name: "vue",
            var: "Vue",
            path: "dist/vue.runtime.min.js"
          },
          {
            name: "vue-router",
            var: "VueRouter",
            path: "dist/vue-router.min.js"
          }
        ]
      }
    ]);

    if (process.env.NODE_ENV === "production") {
      // html minify settings for GAS
      config.plugin("html").tap(args => {
        args[0].minify.removeAttributeQuotes = false;
        args[0].minify.removeScriptTypeAttributes = false;
        return args;
      });
    }
  }
};

こちらに記載されていたもののパクりです。 とても感謝してます。

qiita.com

これにより、vueで書いてもgas上で動かせるようになりました。

claspインストールし、2で作成したプロジェクトの成果物をpushできるようにする

qiita.com

こちらを参考にclaspを使えるようにした。

qiita.com

また、こちらを参考に appsscript.json 等々を配置

package.jsonscript"push": "vue-cli-service build && clasp push -f" を追記してdist以下の生成とそれらのpushをひとまとめにするとラクでした。

4.POSTで書き込みをしようとするのではなく、 google.script.run.○○ で実行する

一番大変なところでした。 詳細は下に記載するとして、まず結論としてページのview部分のファイルはこのようになりました。 *CSSフレームワークにbulmaを使用してます

// index.vue
<template>
  <div class="container">
    <h1>Input Your Data</h1>
    <div class="field">
      <div class="control">
        <input class="input is-info" type="text" placeholder="Text" v-model="text">
      </div>
    </div>
    <div class="field">
      <div class="control">
        <a class="button is-link" @click="onClick">Submit</a>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "Index",
  data() {
    return {
      text: ""
    };
  },
  methods: {
    onClick: function(e) {
      google.script.run.addData(
        JSON.stringify({
          text: this.text,
        })
      );
   }
};
</script>

Code.js(google apps script)

function doGet() {
  return HtmlService.createHtmlOutputFromFile("index").addMetaTag(
    "viewport",
    "width=device-width, initial-scale=1"
  );
}

function addData(rawParams) {
  Logger.log("addData");
  var params = JSON.parse(rawParams);
  var ss = SpreadsheetApp.getActive();
  var sheet = ss.getActiveSheet();
  var values = [params.text];
  sheet.appendRow(values);
}

どうにかしてPOSTでできないかと考えていましたが、普通に google.script.run.hogehoge とやったら動きました。

詰まったところ : POST編

詰まった要因として一番大きかったところはPOSTでリクエストを飛ばしてどうにかできないかと考えていたところでした。

一番最初に考えたこと

axiosを入れて普通にpostしようとした

axios.post("https://script.google.com/macros/s/AKfxxxxx/exec", params)

->405エラー OPTIONSのmethodで飛ばされてしまっているらしい。

stackoverflow.com

次に考えたこと

それだと通らないようだったので application/x-www-form-urlencoded で投げるように修正してみました。

const params = new URLSearchParams();
params.append("test", this.test);
axios.post("https://script.google.com/macros/s/AKfxxxxx/exec", params)

Access to XMLHttpRequest at 'https://script.google.com/macros/s/AKfxxxxx/exec' from origin 'https://n-hv3nxxxxx-script.googleusercontent.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

???
どうやらoriginが違う・・・
ブラウザでアクセスしているURLも https://script.google.com/macros/s/AKfxxxxx/exec にもかかわらず、originが違う認識になっている。
デベロッパーツール開いて window.location.href 等でURLを見てみても https://script.google.com/macros/s/AKfxxxxx/exec になっているのでなかなか不思議な現象。
きっとセキュリティ的な問題でこういう仕様になっているのだろうと諦めた。

結果としておかげさまで google.script.run にたどりつけたのでよかったといえばよかった。
そして公式のドキュメントにも実はちゃんと書いていた。

developers.google.com

詰まったところ : Lambda編

ブラウザから叩こうとするからCORSで阻まれるので、何らかの形でサーバー側から実行できたらいいじゃんと考え、なんとなくlambdaでできないかと試してみた。

qiita.com

紆余曲折はあったものの、上記を参考にして一旦動くようにはなったが、定期的にtokenのリフレッシュをしなければいけない、というところで少し面倒+そこまでして動かしたいものではないなということで断念。

その後いろいろ試してみたら

// Lambda
const request = require("request");

const headers = {
  "Content-type": "application/json"
};

const dataString = '{"value":"aaa"}';

var options = {
  url:process.env['url'],
  method: "POST",
  headers: headers,
  body: dataString
};

function callback(error, response, body) {
  if (!error && response.statusCode == 200) {
    console.log(body);
    const response = {
    statusCode: 200,
    body: JSON.stringify("Hello from Lambda!")
  };
  return response;
  }else{
    console.error(error)
  }
}

exports.handler = event => {
  console.log("POST gas to sheet");
  request(options, callback);
  console.log("Finished");
  // TODO implement
  
};

だいぶてきとーに書いてますが、普通に request で飛ばしたらいけました。
tokenとか全然関係ないのか・・・

余談

実装してみたサンプルコード

github.com

jsでパスワードのvalidationのようなものを書いてみた

ちょっとしたvalidationのようなものを書いてみたので、備忘録として残しておきたい。

前提

HTMLはシンプルに

<input type="password" id="passwordInput"/>

こんなかんじの想定です。
あとjs上での下処理は

let passwordText = document.getElementById('passwordInput').value;

とします。

文字数の確認

passwordの文字が 8文字以上 で良しとしたい場合の想定です。 これはシンプルにlengthで取れますね

if(passwordText.length < 8){
  // 何らかのエラー処理
}else{
  // 正常時の処理 
}

HTMLタグのmaxlength

そういえばHTML5の仕様でタグ上に記載することができますね。

<input type="password" id="passwordInput" maxlength="8"/>

こうすることでjs側での処理は不要になりますね。

同じ文字がいないかどうか

aa11 みたいに連続した文字がいた場合にエラーとしたいときのロジックです。

let passwordTexts = passwordInput.split('');
let previousCharacter = '';
let conuter = 1;
let hasSameCharacter = passwordTexts.some((character) => {
  if(character === previousCharacter){
    conuter++;      
  }else{
    previousCharacter = character;
    conuter = 1;      
  }
  return conuter === 2
});
if(hasSameCharacter){
  // エラー処理
}

特定の文字入力があるかどうかの判定

今回の想定では英字と数字、どちらも入ってるかどうかチェックするというものです。

let strMutcher = passwordText.match(/[a-z]/gi);
let numMutcher = passwordText.match(/[0-9]/gi);
if(strMutcher === null || numMutcher === null){
 // エラー時処理等
}

match でokだった場合は配列が返ってくるので、できれば .length > 0 あたりで判定したいところなのですが、
NGだった場合にnullしか返してこないので上記のような書き方をしました。
(個人的にもう少し改良の余地はあるように思える)
(正規表現がちょっと適切ではない気がするので、もっと勉強が必要)

HTMLタグで正規表現

<input type="password" pattern="^[0-9A-Za-z]+$">

と、いったかんじでタグ上でも正規表現を用いてチェックすることができるようです。 www.htmq.com

ちょっとしたポップアップも出て、便利ですね。
ただしこれ、formタグとセットで使わなければいけないので(ポップアップが出るのもきっとsubmitされるとき)
ページの作り方によっては使いにくいかもしれません。

余談

HTMLやjsでvalidationをする方法を上に書きましたが、結局クライアント側ではどうとでもできるので、本格的な処理はやはりjsで書かなければいけませんね。

あとはHTMLとjs、双方向からできるにしても、どの処理がどっちで担当しているか、というのがごちゃ混ぜになると管理が大変になると思うので、 個人的にはある程度jsに寄せたいなと思う派です。

以上、備忘録でした

Vue CLI3のproductionでconsole.logを消した方法

やりたかったこと

本題の通りなのですが、productionの状態でbuildしたソースからよろしくconsole.logを消せないかなと思ったのでいろいろ調べてみました。

結論

uglifyjs-webpack-plugin を入れて、vue.config.jsにconsole.logを消す設定を書く。

pluginのインストール

npm i -D uglifyjs-webpack-plugin

vue.config.jsに書き足す

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const isProd = process.env.NODE_ENV === "production";
module.exports = {
  configureWebpack: {
    optimization: {
      minimizer: isProd ? [
        new UglifyJsPlugin({
          uglifyOptions: {
            compress: {
              drop_console: true
            },
          }
        })
      ] : []
    }
  }
}

こちらで発見しました

forum.vuejs.org

所感

これを試したのは個人的につくった実装物なので、一旦これでいいんですが、もう少し大きい実装で、コードを難読化したら発生したbugとかに関してはlogがでなくてちょっとつらいのかなと思いました。
もう少しいろいろ設定ができそうなので、折を見て試して追記できたらと思います

IEでreadonlyのinputにcssを当てたくなった時の対処

日々大きな壁として立ちはだかるIEくんに対して頑張ったお話のひとつです。

よくある実装のひとつだと思うのですが、そのinputがreadonlyだったときに、ぱっと見でreadonlyだと識別できるようにスタイルを変えたいというところの実装をしていました。

結論

input[readonly] {
  background-color: gray
}

で対処できました。
(正直当たり前といえば当たり前なのですが)

もともとやっていたこと

input:read-only {
  background-color: gray
}

Chromeだとこれで動いていたんですよね。
そしていざIEで確認してみたところ、ちゃんと効いてない。

調べてみたところ、 :read-only の擬似クラスはIEではサポートされてないとのことでした。
(なるほどそれは動かない 笑)

developer.mozilla.org

興味深かったところ

input[type="text"][readonly] {
  background-color: gray
}

といった形で [ ] を2つつなげても動いた・・・
まあ確かに

input:read-only.hoge-class {
  background-color: gray
}

みたいな形でつなげても動作するので、こちらも当たり前といえば当たり前か・・・(CSSセレクターももう少し勉強が必要ですね)

いつまで動いてるかわかりませんが、下記のjsFiddleは実験物

jsfiddle.net

はじめてのレポジトリにgit pushするときのお作法

やりたいこと

gitでレポジトリ作って、ローカルでひとまずの実装をしたあとにgitにあげる
なんだかんだ頻度がそんなに高くないので忘れてしまう・・・

0. gitのレポジトリつくり

割愛。
そういえばgithubもprivateのレポジトリがタダで使えるようになったのはありがたい。

1. git init

ローカルのリポジトリとして機能させるために

git init

その後適当に実装

2. add と commit

git add .
git commit -m "messages"

で修正をcommit作成

3. remote add

git remote add origin git@github.com:....

でremoteに登録

4. push

あとは

git push 

ついでに

別ブランチきって最初にpushしようとするとそのときもまた怒られる。 その際は

git push --set-upstream origin branch-name

でリモートブランチをつくる

gitの関連書籍

正直基本的な操作はぐぐったらすぐ出てくるので本読んでまでどうにかするものでもない気がしている。(もちろんちゃんと学ぶことは重要)
下の本は個人的には読みやすかったが SourceTree ベースでgitの操作が進んでいたので、コマンドラインでの操作について学びたかったときには少し違った。 www.amazon.co.jp

こちらについては結構詳細に書いてあったのでよかった印象 www.amazon.co.jp