GAミント至上主義

Web Monomaniacal Developer.

Google Apps Scriptのウェブアプリでaxiosでリクエストする時にハマったメモ

シニアジョブで簡単な来客記録システムを作りあたり、Google Workspaceを使っているので、
NuxtJSでUI作って、Google スプレッドシートとGASでいけるんじゃね?
と思ってやってみたら、ちょっとハマったものの出来たのでメモ。

f:id:uyamazak:20210218134626p:plain
※画面は開発中のものです(圧倒的いらすとや感)

NuxtJSの方は今回は省略。

結論

設定ちゃんとすれば、普通のURLに普通のリクエストでスプレッドシートに書き込むようなAPIが簡単にできる。
認証エラーでもCORSエラーになる罠に気をつける。

ログを書き込むスプレッドシートをつくる

特に何の変哲もないやつ作ります。
f:id:uyamazak:20210218132559p:plain

スクリプトを書く

ツール → スクリプトエディタ でGASの入力画面へ。

今回はGETリクエスト使うのでdoGet()という名前にする必要があります。

詳細は公式。GAS関係は結構デタラメな古い情報が検索に溢れているので注意しましょう。
https://developers.google.com/apps-script/guides/web


名前、会社名とかをGETパラメーターで受け取るようにして雑にこんな感じになりました。
認証とかライブラリインストールとか省けるのでいいですね。
空白行とかできるとアレなので簡単に有無チェックだけはしておきます。

function doGet(e) {
  const ss = SpreadsheetApp.getActive().getSheetByName('来客ログ');
  if (!e) {
    return ContentService.createTextOutput('{result:"NG", error: "eventがありません"}')
  }
  const params = e.parameter
  const name = params.name
  if (!name) {
    return ContentService.createTextOutput('{result:"NG", error: "nameは必須です"}')
  }
  const description = params.description
  if (!description) {
    return ContentService.createTextOutput('{result:"NG", error: "descriptionは必須です"}')
  }
  const company = params.company

  ss.appendRow([new Date(), company, name, description]);
  return ContentService.createTextOutput('{result:"OK"}');
}

デプロイでウェブアプリを選択

デプロイには実行可能 APIウェブアプリの2つがあり、名前的に実行可能 APIの方かな?と思ったけど404になってだめでした。
ウェブアプリの方でした。

アクセスできるユーザーを"全員"にする

この画面で全員にしておかないと認証画面にとばされaxiosでCORSエラーになります。
開発者ツールでリクエストをよく見れば分かるものの、認証エラーではなくCORSエラーで来るのが曲者でした。
f:id:uyamazak:20210218133543p:plain

全員と社内だとURLが変わるので注意しましょう。
また設定変更後デプロイしないと反映されません。

axiosでリクエストする

今回はNuxtJSだったのでプラグインでメソッドを生やしました。
上記で取得したURLはnuxt.config.jsのenvに追加。
あとは必要なパラメーターをつけるだけ。

plugins/gas-api.js

import axios from 'axios'
async function addSpreadSheet(name, company, description) {
  await axios.get(`${process.env.gasApiUrl}`, {
    params: { name, company, description },
  })
}

export default (_, inject) => {
  inject('addSpreadSheet', addSpreadSheet)
}

あとはコンポーネントのmethodsでこんな感じのを追加して使いました。

send() {
      this.$refs.form.validate()
      this.isLoading = true
      if (this.valid) {
        // 送信処理
        try {
          await this.$addSpreadSheet(this.name, this.company, 'テスト')
        } catch(e) {
          this.isLoading = false
          this.$router.push('error')
          return
        }
        this.isLoading = false
        this.$router.push('thanks')
      }
    },

Puppeteer v7.1.0でViewportの型エラーが解消しないので調査したログ

結論

mainに入ってるけどまだリリースされてないだけだった。
リリースちゃんと読もう。
TypeScriptでPuppeteerガンガン使いましょう。

経緯

Puppeteerはv6でTypeScriptの型がビルドインになり、@types/puppeteerが必要なくなりました・・・だと良かったんですがリリースノートに書いてあるとおり、まだ型周りの問題、改善点が山積みのようです。

下記の趣味レポジトリ、Puppeteerを使ったPDFサーバーでも型エラーが出るようになりました。

Bump puppeteer from 5.5.0 to 7.1.0 by dependabot · Pull Request #117 · uyamazak/hc-pdf-server · GitHub


型 Viewport がexportされていなかったようなので下記Issueを立てたら、
[7.0.4] Can't import type Viewport · Issue #6876 · puppeteer/puppeteer · GitHub

下記プルリクで対応されました。
fix: expose `Viewport` type by jackfranklin · Pull Request #6881 · puppeteer/puppeteer · GitHub

v7.1.0がリリースされたので試してみたところ、別のエラーが出るようになりました。

src/types/hc-pdf-server.d.ts:1:10 - error TS2459: Module '"../../node_modules/puppeteer/lib/types"' declares 'Viewport' locally, but it is not exported.

v7.1.0で型宣言ファイルを確認すると、上記プルリクでexportが増えたものの、相変わらずexportされておらず問題になっているようです。

↓ namespaceの入れ子になってるのでこっちは関係ないかも
node_modules/puppeteer/lib/types.d.ts:15996

       /**
         * Viewport for capturing screenshot.
         */
        export interface Viewport {
            /**
             * X offset in device independent pixels (dip).
             */
            x: number;
            /**
             * Y offset in device independent pixels (dip).
             */
            y: number;
            /**
             * Rectangle width in device independent pixels (dip).
             */
            width: number;
            /**
             * Rectangle height in device independent pixels (dip).
             */
            height: number;
            /**
             * Page scale factor.
             */
            scale: number;
        }

node_modules/puppeteer/lib/types.d.ts:24286

declare interface Viewport {
    width: number;
    height: number;
    deviceScaleFactor?: number;
    isMobile?: boolean;
    isLandscape?: boolean;
    hasTouch?: boolean;
}

またIssueを立てる前に、なぜこんなことが起きてるか知りたいので調査することにしました。

上記のtypes.d.tsは、レポジトリには入っておらず、出どころがわからなかったので、ローカルにclone

git clone https://github.com/puppeteer/puppeteer.git


単純に2箇所でexportしてるのかなぁと疑ったけど、検索した感じ上記Issueの場所以外では見つからず。

package.jsonを見るとこんだけscriptsが。

"scripts": {
    "test-browser": "wtr",
    "test-browser-watch": "wtr --watch",
    "unit": "npm run tsc-cjs && mocha --config mocha-config/puppeteer-unit-tests.js",
    "unit-debug": "npm run tsc-cjs && mocha --inspect-brk --config mocha-config/puppeteer-unit-tests.js",
    "unit-with-coverage": "cross-env COVERAGE=1 npm run unit",
    "assert-unit-coverage": "cross-env COVERAGE=1 mocha --config mocha-config/coverage-tests.js",
    "funit": "PUPPETEER_PRODUCT=firefox npm run unit",
    "test": "npm run tsc && npm run lint --silent && npm run unit-with-coverage && npm run test-browser",
    "prepare": "node typescript-if-required.js",
    "prepublishOnly": "npm run build",
    "dev-install": "npm run tsc && node install.js",
    "install": "node install.js",
    "eslint": "([ \"$CI\" = true ] && eslint --ext js --ext ts --quiet -f codeframe . || eslint --ext js --ext ts .)",
    "eslint-fix": "eslint --ext js --ext ts --fix .",
    "commitlint": "commitlint --from=HEAD~1",
    "lint": "npm run eslint && npm run build && npm run doc && npm run commitlint",
    "doc": "node utils/doclint/cli.js",
    "clean-lib": "rm -rf lib",
    "build": "npm run tsc && npm run generate-d-ts",
    "tsc": "npm run clean-lib && tsc --version && npm run tsc-cjs && npm run tsc-esm",
    "tsc-cjs": "tsc -b src/tsconfig.cjs.json",
    "tsc-esm": "tsc -b src/tsconfig.esm.json",
    "apply-next-version": "node utils/apply_next_version.js",
    "test-install": "scripts/test-install.sh",
    "generate-d-ts": "api-extractor run --local --verbose",
    "generate-docs": "npm run generate-d-ts && api-documenter markdown -i temp -o new-docs",
    "ensure-correct-devtools-protocol-revision": "ts-node -s scripts/ensure-correct-devtools-protocol-package",
    "test-types-file": "ts-node -s scripts/test-ts-definition-files.ts",
    "release": "node utils/remove_version_suffix.js && standard-version --commit-all"
  },

buildでTypeScript周りのファイルが作られそうです。

問題のlib/types.d.tsファイルはgenerate-d-tsの

"generate-d-ts": "api-extractor run --local --verbose",

api-extractorを使っている模様。

api-extractorってなんだろ。

Microsoftのツールでした。よくわからん。

api-extractor.com


よくわからないので出力されたtypes.d.tsを見てみると・・・
lib/types.d.ts:24291

export declare interface Viewport {

あれ!?exportされてる。どういうこっちゃ・・・。

ローカルの古いファイルとかキャッシュの問題かと思ったけどGitHub Actions上でも再現してるし、実際rm -rf node_modulesは何度もやってる。

おかしいぞと念の為git logを確認したところ

$ git log
commit be7c22933c1dcf5eee797d61463171bd0ef44582 (HEAD -> main, origin/main, origin/HEAD)
Author: Jack Franklin <jacktfranklin@chromium.org>
Date:   Fri Feb 12 12:32:27 2021 +0000

    fix: expose `Viewport` type (#6881)
    
    Also includes drive-by when I subbed `@default` (not valid) to
    `@defaultValue` (valid!) in a few places and exposed types from
    `Coverage.ts` so they get exposed too.
    
Date:   Fri Feb 12 12:32:27 2021 +0000

    fix: expose `Viewport` type (#6881)
    
    Also includes drive-by when I subbed `@default` (not valid) to
    `@defaultValue` (valid!) in a few places and exposed types from
    `Coverage.ts` so they get exposed too.
    
    Fixes 6876.

commit 29c059427e377f1e3772b182363b93b484b06ace
Author: Jack Franklin <jacktfranklin@chromium.org>
Date:   Fri Feb 12 11:19:06 2021 +0000

    chore: bump to 7.1.0-post (#6880)

commit a681aac7e3b7326dc41ecfc895068c30549d8b4d (tag: v7.1.0)
Author: Jack Franklin <jacktfranklin@chromium.org>
Date:   Fri Feb 12 10:51:43 2021 +0000

    chore(release): mark v7.1.0 (#6879)

commit a5e174f696eb192c541db64a603ea5cdf385a643
Author: Jack Franklin <jacktfranklin@chromium.org>
Date:   Thu Feb 11 15:44:56 2021 +0000

    fix: expose puppeteer.Permission type (#6856)
    
    * fix: expose puppeteer.Permission type
    
    A regression in our own types when compared to @types/puppeteer - we
    needed to expose the permissons.

commit ad5935738d869cfce386a0d28b4bc6131457f962
Author: Alex Rudenko <OrKoN@users.noreply.github.com>
Date:   Thu Feb 11 14:59:50 2021 +0100

    feat(page): add color-gamut support to Page.emulateMediaFeatures (#6857)

修正した

fix: expose `Viewport` type (#6881)

chore(release): mark v7.1.0 (#6879)

より後に入ってる・・・。

というわけで結論に戻りつつも、いろいろ勉強になりましたとさ。

プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発

プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発

  • 作者:Boris Cherny
  • 発売日: 2020/03/16
  • メディア: 単行本(ソフトカバー)

あなたの欲しかった小数点以下"切り捨て"はMath.floorではなくMath.truncかもしれない

JavaScript 小数点切り捨て」で検索するとMath.floor()とparseInt()使う記事が出てきてよろしくないと思ったのでメモ。

結論

小数点以下を削除したいのならMath.trunc()使え。負の時は思ってたのと違うかも。
小数点以下切り下げならMath.floor()


正の値のときは何も問題が無いと思うけど、Math.floorは名前の通り床、下の方に寄せる関数なので整数部分が変わってしまう。
日本語で切り捨てと言ってしまっているのが問題で、切り下げと考えれば大丈夫そう。

Math.floor(100.1)
100
Math.floor(-100.1)
-101

Math.floor() - JavaScript | MDN

Math.ceilも天井、上に寄せる関数なので同じ現象が起きる。けど日本語だと小数点以下切り上げと呼ばれるので誤解は少ないかもしれない。

Math.ceil(100.1)
101
Math.ceil(-100.1)
-100

Math.truncであればtruncate、文字通り捨ててくれるので正負に関わらず整数部分は変わらない。

Math.trunc() - JavaScript | MDN
一般的な金額の用途で「小数点以下切り捨ててほしい」ってだいたいこっちな気がする

Math.trunc(100.1)
100
Math.trunc(-100.1)
-100

parseIntを使うと書いてある記事もあるけど、基数指定しないと問題起きるし、この用途ではふさわしくないと思う。
[JavaScript] 8をparseIntすると0になる問題の対処法

一般的な呼び方じゃないかもしれないけど、切り捨てと切り下げを日本語でもちゃんと分けて考えれば良さそう

小数点以下切り捨て → Math.trunc
小数点以下切り下げ → Math.floor
小数点以下切り上げ → Math.ceil