- 投稿日:2019-09-09T22:38:15+09:00
材料からの料理のランダム抽出を行うPGM
材料から料理のランダム抽出を行うPGMです
githubURL: https://github.com/NanjoMiyako/RyoriGatya
サンプルページURL: https://nanjomiyako.github.io/RyoriGatya/
参考ページ
GAS (Google Apps Script) でファイル出力を行う - HRR Co., Ltd.リンクを新規タブで開かせる方法(HTML) | monopocket blog
使い方
まずデータインポートタブから材料・料理のJSONデータをインポートしてから
検索を行います。JSONデータはGASのシートからoutput()のマクロを実行して作成します。JSONデータ作成シートURL: https://docs.google.com/spreadsheets/d/1Krh4gF3TeNxEdxNG9VfN0KZ8DoqGpd5LC20AJFGxG9Q/edit?usp=sharing
- 投稿日:2019-09-09T22:22:02+09:00
React.memoしたコンポーネントのdisplayNameを取得する
Reactのデバッグ用に、コンポーネントの名前を出力していたのですが、その過程で
React.memoしたコンポーネントは特殊な扱いが必要でした。
displayNameとは以前にべからず集でも触れましたが、コンポーネントに
displayNameがセットしてあると、それがデバッグ時にコンポーネント名として表示されます。そして、React公式サイトにあるコード片にも
WrappedComponent.displayName || WrappedComponent.nameのようなコードがあるように、関数宣言やクラスなどでnameが設定されていれば、それで代用できます。
React.memoを使った場合ところが、
React.memoを使った場合、displayNameはセットされません(もちろん関数生成ではないので、自動的にnameが付くこともありません)。なので、displayName || nameのコードでは何も取れません。ただ、Reactのデバッグツールでは
Memo(WrappedComponent)のような名前がしっかりと出ています。このような名前を取得できないか調べてみました。
react-isとはそして、調べてみると、
React.memoで生成したコンポーネントにはtypeというプロパティがあって、ここにもとのコンポーネントが来ることが判明しました。あとはメモ化コンポーネントを識別できれば、要件は片付きます。もちろん内部データにアクセスして調べられなくもないのかもしれませんが、それをやっていると将来的に内部構造が変化したときに死にます。そこでReactチームが公式に用意している手法として、
react-isというライブラリがあります(GitHub)。以下のようなメソッド・定数が用意されています。
ReactIs.isValidElementType(arg)…argがReactコンポーネントにできるもの(タグ名の文字列・関数コンポーネント・クラスコンポーネントなど)かを判定するReactIs.typeOf(arg)…argの種類を、以下の定数のどれかで返す
ReactIs.ConcurrentModeReactIs.ContextConsumerReactIs.ContextProviderReactIs.ElementReactIs.ForwardRefReactIs.FragmentReactIs.MemoReactIs.LazyReactIs.PortalReactIs.ProfilerReactIs.StrictModeReactIs.SuspenseReactIs.is***(上の定数に対応したメソッドがあります)…それぞれの種類かを判定するなお、どういうわけか
import ReactIs from 'react-is'の形では読み込めず、import {isMemo} from 'react-is'と単品で呼ぶか、全部読み込む場合はimport * as ReactIs from 'react-is'とする必要があります。実際に書いてみた
素材が揃ったので、あとはコードに起こすだけです。
import {isMemo} from 'react-is'; function getDisplayName(component) { const {name, displayName} = component; // displayNameがついていればそれを採用 if(displayName) return displayName; // メモ化コンポーネントの場合 if(isMemo(component)) return `Memo(${getDisplayName(component.type)})`; // あとはnameなどをチェック return name || null; }
- 投稿日:2019-09-09T21:49:38+09:00
【JavaScript】アロー, コールバック関数等の覚書き
JavaScriptではオブジェクトを生成してメンバと値を紐づけることができる。
他の言語ではハッシュ、連想配列、辞書型と呼ばれる形式に近い。//次のオブジェクト(user)ではnameというメンバに'hoge'という値を設定。 const user = {name: 'hoge', age: 100} console.log(user.name) //結果 hogeオブジェクトにメンバと値を追加する場合は次のようにする。
const user = {name: 'hoge', age: 100} //オブジェクトにメンバと値を設定 user.sex = "man" console.log(user.sex) //結果 man関数もオブジェクトとして登録できる。
const user = {name: 'hoge', age: 100} user.func = function() { console.log("FUNC") } //オブジェクトから関数を呼び出すときは`メンバ()`とするっぽい user.func() //結果 FUNCアロー関数で書いてみる。
イメージとしては、function()を() =>と書き換えるかんじ。const user = {name: 'hoge', age: 100} //アロー関数で書くと次の通り user.func = () => { console.log("FUNC") } user.func() //結果 FUNC引数を渡すときは次のように書ける。
//省略 user.func = (name) => { console.log(`${name}`) } user.func("tes") //結果 tes引数に関数を定義してコールバックする。
const call = (callback) => { console.log("コールバック関数を呼び出す。"); callback(); }; // 引数で関数を定義して渡してください call(() => { console.log("callback!"); }); //実行結果 コールバック関数を呼び出す。 callback!コールバック関数に引数を渡す
const call = (callback) => { callback("私の年齢は", 14); }; // 関数callの引数の中で2つの引数を取る関数を追加してください call((name, age) => { console.log(`${name}は${age}歳です。`); }); //実行結果 私の年齢は14歳です。
- 投稿日:2019-09-09T19:39:03+09:00
JavaScript:querySeletor,querySeletorAll
querySeletorメソッド説明
DOMはHTMLの構造、その中のタグからJavaScriptで一つ選べるメソッドはquerySeletorがある。
HTML
以下の例はclassとid両方がある。
querySeletor.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="css/style.css"> <title>Document</title> </head> <body> <h1 class="titleClass"><em>apple</em></h1> <!-- el !--> <h1 id="titleId"><em>orange</em></h1> <!-- el2 !--> <script src="js/all.js"></script> </body> </html>JavaScript
まず、documentはHTMLの内容そしてquerySelectorを引き続いで目標のタグを指定される、便利になる為に、変数代入することは必要である。
queryse.jsvar el = document.querySelector('.titleClass em'); el.textContent ='yellow'; var el2 = document.querySelector('#titleId'); el2.textContent='red';それでは、変数保存した内容メソッドはtextContentを引き継いで、変更したいの内容を代入できます。ちなみに、classの指定符号は『.classname tagname』、idは『#idname』を使われる。
querySeletorAllとは
querySeletorは便利けれども、二つ以上の条件はできません。
not_effect_query.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="css/style.css"> <title>Document</title> </head> <body> <h1 class="titleClass"><em></em></h1> <h1 class="titleClass"><em></em></h1> <script src="js/all.js"></script> </body> </html>以上は同じタグを選びたい、一応querySeletorを試す。
not_effect_query.jsvar el = document.querySelector('.titleClass em'); el.textContent ='yellow';修正後
この状況を会ったら、querySelectorAllはなおしてみると、複数クラス指定できた。
querySelectorAll.jsvar el = document.querySelectorAll('.titleClass em'); el[0].textContent = '123'; el[1].textContent = '123'; var elLen = el.length; for(var i = 0;i<elLen;i++){ el[i].textContent = i+'123'; }実際はquerySelectorAllに指定したものは配列になる、それでは配列のインデックスで内容を操作できる、ちなみに、配列は長くなればループを使わればはもっと便利になる。
- 投稿日:2019-09-09T19:07:20+09:00
HTML5にpptxを挿入します
pptxファイルをフォルダに入れる
何もない
bxSliderをダウンロードする
ホームページ:
https://bxslider.com/
installボタンを押して、インストールページにアクセスする。最も簡単な方法でやる
「The Easy Way」が書かれた部分のコードをコピーして、headとbodyをそれぞれHTML5ファイルに記載する。
body部分を修正する
<div class="slider"> <div>I am a slide.</div> <div>I am another slide.</div> </div>上記コード、真ん中の2行をpptxファイルの情報に書き換える。srcの部分を、pptxのディレクトリに書き換える。
例:<div class="slider"> <div><img src="/stats/pic/P1.png" width="750px" height="400px"></div> <div><img src="/stats/pic/P2.png" width="750px" height="400px"></div> </div>imgタグは、別の属性もある。ネットで調べて出てくる。
HTML5にiframeタグを使ってやりたい時
HTML5ファイルに挿入したい場所でiframeタグを書く。
例:homepage.html<html> <head> ... <iframe src="slideshow.html" width="750px" height="480px"></iframe> ... </head> </html>slideshow.html<html> <head> <link rel="stylesheet" href="https://cdn.jsdelivr.net/bxslider/4.2.12/jquery.bxslider.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/bxslider/4.2.12/jquery.bxslider.min.js"></script> <script> $(document).ready(function(){ $('.slider').bxSlider(); }); </script> </head> <body> <div class="slider"> <div><img src="/stats/pic/P1.png" width="750px" height="400px"></div> <div><img src="/stats/pic/P2.png" width="750px" height="400px"></div> </div> </body> </html>iframeタグを使ってできたやつ
(これはキャプチャ)
- 投稿日:2019-09-09T19:07:20+09:00
html5にpptxを挿入します
pptxファイルをフォルダに入れる
何もない
bxSliderをダウンロードする
ホームページ:
https://bxslider.com/
installボタンを押して、インストールページにアクセスする。最も簡単な方法でやる
「The Easy Way」が書かれた部分のコードをコピーして、headとbodyをそれぞれHTML5ファイルに記載する。
body部分を修正する
<div class="slider"> <div>I am a slide.</div> <div>I am another slide.</div> </div>上記コード、真ん中の2行をpptxファイルの情報に書き換える。srcの部分を、pptxのディレクトリに書き換える。
例:<div class="slider"> <div><img src="/stats/pic/P1.png" width="750px" height="400px"></div> <div><img src="/stats/pic/P2.png" width="750px" height="400px"></div> </div>別の属性もある。ネットで調べて出てくる。
HTML5ファイルにiframeタグでやりたい時
HTML5ファイルに挿入したい場所でiframeタグを書く。
例:homepage.html<iframe src="slideshow.html" width="750px" height="480px"></iframe>slideshow.html<html> <head> <link rel="stylesheet" href="https://cdn.jsdelivr.net/bxslider/4.2.12/jquery.bxslider.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> <script src="https://cdn.jsdelivr.net/bxslider/4.2.12/jquery.bxslider.min.js"></script> <script> $(document).ready(function(){ $('.slider').bxSlider(); }); </script> </head> <body> <div class="slider"> <div><img src="/stats/pic/P1.png" width="750px" height="400px"></div> <div><img src="/stats/pic/P2.png" width="750px" height="400px"></div> </div> </body> </html>できたやつ(これはキャプチャ)
- 投稿日:2019-09-09T18:31:47+09:00
【Nuxt.js】WebアプリSHISHOWを開発したときの備忘録②Nuxt.js, firebase導入編
Nuxt.jsの導入
npmはすでに導入されているものとします。
npx create-nuxt-appで新しくプロジェクトを開始する。
npx create-nuxt-app shishowcreate-nuxt-app v2.10.1 ✨ Generating Nuxt.js project in shishow ? Project name shishow ? Project description My legendary Nuxt.js project ? Author name Papillon6814 ? Choose the package manager Npm ? Choose UI framework None ? Choose custom server framework None (Recommended) ? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Choose test framework None ? Choose rendering mode Universal (SSR) ? Choose development tools jsconfig.json (Recommended for VS Code)サーバーサイドレンダリングがしたかったのでSSRの方を選んでいます。
今回はCSSフレームワークも使っていません。cd shishowそしてshishowディレクトリに移動して,
npm run devnpm run devコマンドでプロジェクトを起動します。
npm run buildコマンドは,プロジェクトをfirebaseに
アップロードするときに使ったのでこの時は使わなくても大丈夫でした。第3回に続きます
- 投稿日:2019-09-09T18:31:47+09:00
【Nuxt.js】WebアプリSHISHOWを開発したときの備忘録②Nuxt.js導入編
Nuxt.jsの導入
npmはすでに導入されているものとします。
npx create-nuxt-appで新しくプロジェクトを開始します。
npx create-nuxt-app shishowcreate-nuxt-app v2.10.1 ✨ Generating Nuxt.js project in shishow ? Project name shishow ? Project description My legendary Nuxt.js project ? Author name Papillon6814 ? Choose the package manager Npm ? Choose UI framework None ? Choose custom server framework None (Recommended) ? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Choose test framework None ? Choose rendering mode Universal (SSR) ? Choose development tools jsconfig.json (Recommended for VS Code)サーバーサイドレンダリングがしたかったのでSSRの方を選んでいます。
今回はCSSフレームワークも使っていません。cd shishowそしてshishowディレクトリに移動して,
npm run devnpm run devコマンドでプロジェクトを起動します。
npm run buildコマンドは,プロジェクトをfirebaseに
アップロードするときに使ったのでこの時は使わなくても大丈夫でした。第3回に続きます。
- 投稿日:2019-09-09T17:26:37+09:00
ドットインストール復習②~ヘルスケアwebサービスを自分で作る医者の日記~
ドットインストールの復習
ストップウォッチアプリ
やり直すと
どれだけ一回だけでは理解できていないか愕然とするが
確実に学びになる。見えている深さが違う。
以下
ポイントをコメントアウトにて記録main.js'use strict'; { const timer = document.getElementById('timer'); const start = document.getElementById('start'); const stop = document.getElementById('stop'); const reset = document.getElementById('reset'); let startTime; let timeoutId; let elapsedTime = 0; // Date.nowの扱い function countUp() { const d = new Date(Date.now() - startTime + elapsedTime); const m = String(d.getMinutes()).padStart(2, '0'); const s = String(d.getSeconds()).padStart(2, '0'); const ms = String(d.getMilliseconds()).padStart(3, '0'); timer.textContent = `${m}:${s}.${ms}`; timeoutId = setTimeout(() => { countUp(); }, 10); } // ボタン要素のdisableプロパティの操作、表示のオンオフを関数で定義しておく。 function setButtonStateInitial() { start.classList.remove('inactive'); stop.classList.add('inactive'); reset.classList.add('inactive'); } function setButtonStateRunnnig() { start.classList.add('inactive'); stop.classList.remove('inactive'); reset.classList.add('inactive'); } function setButtonStateStopped() { start.classList.remove('inactive'); stop.classList.add('inactive'); reset.classList.remove('inactive'); } setButtonStateInitial(); start.addEventListener('click', () => { if(start.classList.contains('inactive') === true ) { return; } setButtonStateRunnnig(); startTime = Date.now(); countUp(); }); // elapsedTimeはstartを押した時からstopを押した時までの時間を保持する。 // +=で多重代入 stop.addEventListener('click', () => { // return;することでinactiveになってる場合にクリックしてもその後の処理はすすまない、無効化しているのと一緒 if(stop.classList.contains('inactive') === true ) { return; } setButtonStateStopped(); clearTimeout(timeoutId); elapsedTime = Date.now() - startTime; }); reset.addEventListener('click', () => { if(reset.classList.contains('inactive') === true ) { return; } setButtonStateInitial(); timer.textContent = '00:00.000'; elapsedTime = 0; }); }styles.css/* heightプロパティとline-heightプロパティをおなじpxにすると要素の中の文字が上下中央揃いになる */ #timer { height: 120px; font-size: 40px; line-height: 120px; margin-bottom: 15px; background: #ddd; }ソフトウェアの機能
と
ユーザーの実際の操作
と
プログラミングの記述法3つの軸がそれぞれ異なる。
これがプログラミングが難しい原因
UMLというのを勉強するとこれがわかるらしい。
いつか勉強したいな。
- 投稿日:2019-09-09T14:28:37+09:00
GAS の CLI ツール「clasp」で TypeScript を扱ってみた
はじめに
いつか触ろうと思って触ってなかった clasp
https://qiita.com/HeRo/items/4e65dcc82783b2766c03
を参考にしながら触ってみた。しかも @google/clasp v1.5.0 から TypeScript をサポートしていて、
トランスパイルも自動的にやってくれるとのこと。但し、TS ファイルから自動的に GS ファイルに変換されたものを
clasp cloneやclasp pullすると、変換後の結果が JS ファイルとしてダウンロードされるらしい。
ので、最初からトランスパイル後のファイルをclasp pushしたら良いのかな?と試してみた記録です。前提
複数ファイルに分割していない場合は直接 push しても、
トランスパイル後のファイルを push しても変わりはなかったので、
複数ファイルを用意して試す。push 対象の TS ファイル
src/code.tsimport { outputLog } from './hoge'; function myFunction() { outputLog(); }src/hoge.tsexport function outputLog(): void { Logger.log('Hello World!'); }
TS ファイルを直接 clasp push した場合
初めに普通にそのまま push するパターン
.clasp.jsonの rootDir で push 対象ディレクトリを指定。$ clasp push └─ src/appsscript.json └─ src/code.ts └─ src/hoge.ts Pushed 3 files.変換された GS ファイル
code.gs// Compiled using ts2gas 3.4.4 (TypeScript 3.6.2) var exports = exports || {}; var module = module || { exports: exports }; //import { outputLog } from './hoge'; function myFunction() { outputLog(); } //# sourceMappingURL=module.js.map↑import 文が勝手にコメントアウトされてた。
ts2gas 3.4.4っていうのを使ってるみたい?hoge.gs// Compiled using ts2gas 3.4.4 (TypeScript 3.6.2) var exports = exports || {}; var module = module || { exports: exports }; function outputLog() { Logger.log('Hello World!'); } exports.outputLog = outputLog; //# sourceMappingURL=module.js.mapmyFunction 実行結果
まぁ普通にいける
トランスパイルした JS ファイルを clasp push した場合
tsconfig.json で
dist以下にトランスパイルファイルを出力するようにした。
合わせて.clasp.jsonの rootDir はdistに変更した。
module="es2015", target="es5"
$ clasp push | Pushing files...Push failed. Errors: { Error: Syntax error: Missing ; before statement. line: 1 file: code at Gaxios.request (/Volumes/sdcard/Bin/.nodenv/versions/10.15.1/lib/node_modules/@google/clasp/node_modules/gaxios/build/src/gaxios.js:70:23) at process._tickCallback (internal/process/next_tick.js:68:7) response: { config: { url: 'https://script.googleapis.com/v1/projects/xxxxxxxxxx/content', method: 'PUT', paramsSerializer: [Function], data: [Object], headers: [Object], params: [Object: null prototype] {}, validateStatus: [Function], retry: true, body: '{"scriptId":"xxxxxxxxxx","files":[{"name":"appsscript","type":"JSON","source":"{\\n \\"timeZone\\": \\"Asia/Tokyo\\",\\n \\"dependencies\\": {\\n },\\n \\"exceptionLogging\\": \\"STACKDRIVER\\"\\n}"},{"name":"code","type":"SERVER_JS","source":"import { outputLog } from \'./hoge\';\\nfunction myFunction() {\\n outputLog();\\n}\\n//# sourceMappingURL=code.js.map"},{"name":"hoge","type":"SERVER_JS","source":"export function outputLog() {\\n Logger.log(\'Hello World!\');\\n}\\n//PropertiesService.getScriptProperties().getProperty(\'HOGE\')\\n//# sourceMappingURL=hoge.js.map"}]}', responseType: 'json', retryConfig: [Object] }, data: { error: [Object] }, headers: { 'alt-svc': 'quic=":443"; ma=2592000; v="46,43,39"', 'cache-control': 'private', connection: 'close', 'content-encoding': 'gzip', 'content-type': 'application/json; charset=UTF-8', date: 'Mon, 09 Sep 2019 04:43:21 GMT', server: 'ESF', 'transfer-encoding': 'chunked', vary: 'Origin, X-Origin, Referer', 'x-content-type-options': 'nosniff', 'x-frame-options': 'SAMEORIGIN', 'x-xss-protection': '0' }, status: 400, statusText: 'Bad Request' }, config: { url: 'https://script.googleapis.com/v1/projects/xxxxxxxxxx/content', method: 'PUT', paramsSerializer: [Function], data: { scriptId: 'xxxxxxxxxx', files: [Array] }, headers: { 'Accept-Encoding': 'gzip', 'User-Agent': 'google-api-nodejs-client/2.0.4 (gzip)', Authorization: 'Bearer ya29.Glx-B1azGu5jyQ_tkABZ9n6mmnDuPnn5uZLM44sOwDMXfAt3mC7Bem5Dmi9joKJ3lmvg7vNy87f37h0muiiccVOdO1Uv-n4YQLqLasp5r03_EzZZAlQ1_AHCt_70bg', 'Content-Type': 'application/json', Accept: 'application/json' }, params: [Object: null prototype] {}, validateStatus: [Function], retry: true, body: '{"scriptId":"xxxxxxxxxx","files":[{"name":"appsscript","type":"JSON","source":"{\\n \\"timeZone\\": \\"Asia/Tokyo\\",\\n \\"dependencies\\": {\\n },\\n \\"exceptionLogging\\": \\"STACKDRIVER\\"\\n}"},{"name":"code","type":"SERVER_JS","source":"import { outputLog } from \'./hoge\';\\nfunction myFunction() {\\n outputLog();\\n}\\n//# sourceMappingURL=code.js.map"},{"name":"hoge","type":"SERVER_JS","source":"export function outputLog() {\\n Logger.log(\'Hello World!\');\\n}\\n//PropertiesService.getScriptProperties().getProperty(\'HOGE\')\\n//# sourceMappingURL=hoge.js.map"}]}', responseType: 'json', retryConfig: { currentRetryAttempt: 0, retry: 3, retryDelay: 100, httpMethodsToRetry: [Array], noResponseRetries: 2, statusCodesToRetry: [Array] } }, code: 400, errors: [ { message: 'Syntax error: Missing ; before statement. line: 1 file: code', domain: 'global', reason: 'badRequest' } ] } └─ dist/appsscript.json └─ dist/code.js └─ dist/hoge.js↑なんかエラー出た。
push しようとしたトランスパイル結果
dist/code.jsimport { outputLog } from './hoge'; function myFunction() { outputLog(); } //# sourceMappingURL=code.js.mapdist/hoge.jsexport function outputLog() { Logger.log('Hello World!'); } //# sourceMappingURL=hoge.js.map↑普通に import/export なやつ。
module="commonjs", target="es5"
$ clasp push └─ dist/appsscript.json └─ dist/code.js └─ dist/hoge.js Pushed 3 files.↑clasp push できた
トランスパイル結果を push して変換された GS ファイル
code.gs"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var hoge_1 = require("./hoge"); function myFunction() { hoge_1.outputLog(); } //# sourceMappingURL=code.js.maphoge.gs"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); function outputLog() { Logger.log('Hello World!'); } exports.outputLog = outputLog; //# sourceMappingURL=hoge.js.map↑requre 系なやつ。
ちなみに dist 以下に出力されたdist/code.jsとdist/hoge.jsと中身は同じ。
ファイル名だけ.gsに変換されている模様myFunction 実行結果
実行ができなかった!
まとめ
import を使わない場合はトランスパイル後の JS ファイルを push しても問題はない。
でも clasp に任せて TS ファイルを直接 push したほうが良さそう。push した TS ファイルは GS ファイルに変換されているので、
次回clasp clone,clasp pull等をすると変換後の GS ファイルがダウンロードされてしまう。
TS ファイルは別途、Git 等で管理したほうが良さそう。
- 投稿日:2019-09-09T14:23:08+09:00
Resemble.jsをとにかく使う
とにかくResemble.jsを使ってみたかったので、最短で動かす方法を調べました。
比較する画像
名称未設定のフォルダの有無です。
環境設定
$ node -v v8.11.3 $ npm -v 6.4.1 $ npm i resemblejs $ npm i fsnodeとnpmが入っている環境に、resemblejsとfsをinstallする。
実行ソース
【Ionic + Electron】Resemble.jsを使って画像比較を行うデスクトップアプリを作成
を参考に、以下のソースで実行const fs = require('fs'); const resemble = require('resemblejs'); // 比較したい画像のパスを指定 const image1 = fs.readFileSync("a.jpg"); const image2 = fs.readFileSync("b.jpg"); resemble(image1).compareTo(image2).onComplete(data => { if (data.misMatchPercentage >= 0.01) { console.log('差分を検知しました。'); fs.writeFileSync("./diff_image.jpg", data.getBuffer()); }else{ console.log("差分なし"); } });
出力結果
結果、以下の画像が作成された。
diff_image.jpg
これで動作確認完了!
- 投稿日:2019-09-09T13:37:34+09:00
Uncaught TypeError: OO.filter is not a function
var apiYokoName = apiMetaYokoList[0].CLASS.filter(function(item, index){ if (item['@code'] == valueData[key]['@time']){ return true; } }); //TypeError: apiMetaYokoList[0].CLASS.filter is not a function修正
var apiYokoName = [apiMetaYokoList[0].CLASS].filter(function(item, index){ if (item['@code'] == valueData[key]['@time']){ return true; } });filterは配列のメソッドなのにCLASSというオブジェクトで使おうとしたからエラーが出た。
- 投稿日:2019-09-09T12:24:35+09:00
iOS13とiPadOSに備える(フロントエンド)
もうじきiOS13、iPadOSが一般公開されますね。
みなさんは自身のサービスで動作確認済ませてますか?この記事では自分が観測した要注意事象と、それに対する対処を紹介します。
※ iOS13 beta8時点の情報です。要注意
プライベートモード判定の裏技が使えなくなった
プライベートモード判定しているサイトは要注意です!!
Apple Developer Forumsにもある内容ですが、iOS11とiOS12では下記のコードでWebSQLがエラーになる場合はプライベートモードと判定することが可能でした。
var isPrivate = false; try { window.openDatabase(null, null, null, null); } catch (_) { isPrivate = true; }しかし、Safari13ではWebSQLのサポートが終了し、プライベートモードを判定するすべがなくなりました。
ちなみに!
Safari13で上記コードがどのように動くかというと・・・
window.openDatabaseが存在しないため常にisPrivate = trueとなります。
プレイベートモードユーザーのアクセスを制限しているようなサービスの場合、Safari13のユーザー全てがアクセス不可となり大惨事です。Safari13未満だけでもプライベートモード判定をしたい場合は、今のうちに次のように書き換えましょう。
var isPrivate = false; if (window.openDatabase) { try { window.openDatabase(null, null, null, null); } catch (_) { isPrivate = true; } }ちなみにChromeでも判定用の裏技がありましたが、最新バージョンのChrome76で既に潰されています。
Chromeのシークレットモード、Webサイト側での検出ができないように修正へ - Engadget 日本版iPadOSはデフォルトのUserAgentがmacOSと同じになる
UserAgentでiOS判定などを行なっているサイトは要注意です!!
iOS Safari13には「デスクトップ用WEBサイトを表示」というオプション機能が追加されていて、iPadOSはそれがデフォルトでONになっています。
その結果、SafariのUserAgentがmacOSと同じものになります。
実際のUserAgent
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15「デスクトップ用WEBサイトを表示」をOFFにすると、従来のiPadのUserAgentとなります。
Mozilla/5.0 (iPad; CPU OS 13_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Mobile/15E148 Safari/604.1ちなみにWebViewのUserAgentもmacOSに偽装されています。
iPhoneの「デスクトップ用WEBサイトを表示」は使えますが、デフォルトOFFなので意図的に設定されない限り問題ありません。
対処としては、タッチイベントに対応しているか否かで判定可能です。
PC用Safariはタッチイベント非対応ですが、iOS Safariは対応しています。
Can I use... Support tables for HTML5, CSS3, etciOS判定の実装例↓
if (Safari判定 && typeof document.ontouchstart !== 'undefined') { // Safari かつ タッチイベント対応 = iOS }IndexedDBの保存容量に上限ができた
ブラウザゲームなどIndexedDBを活用しているサービスは要注意です(・・・滅多なさそうですが)
一般的には「SafariのWebストレージ上限は50MB」という情報が出回っていると思いますが、実際のところはIndexedDBには保存上限がありませんでした。
やろうと思えばストレージを食いつぶすまでデータ保存可能でした(これはこれでヤバイ)Safari13ではついに上限が設定され、1GBあたりで容量エラーが返される事を確認しました。
1GBもあれば十分ですし、ユーザーにとっても安心ですね。ただし!リソース削除だけでは容量エラーが解消されないという怪しい挙動になっています。
リソース削除 & Safariのキャッシュ削除 & 端末再起動 まですればエラー解消できましたが・・・
ソフトウェア側の対処としては、しっかりエラーハンドリングしつつ、容量エラーになってもサービスが継続できるよう設計するしか無いですかね。おまけ
個人的にSafari13で期待している事も紹介したいと思います。
PWAのタスクキルが真っ当な動作になった
iOS12.1まで : PWAが非アクティブになるたびにセッションが切れてしまい、サービスによっては使い物にならない状態
iOS12.2から : 非アクティブでもセッションは維持できるが、タスクキルしてもセッションを切れないという予想外の挙動
iOS13から : 非アクティブでもセッションは維持できるし、タスクキルすればセッションを切れるネイティブアプリ同等の真っ当な挙動になったPWAのカメラ対応できそうな雰囲気だった
iOS13 beta1 PWAでもカメラが使えるようなりました
iOS13 beta2 重大なバグがあったとのことでリバートされました
iOS13 beta8 カメラはまだ使えません対応できそうな雰囲気だったのに!リリース版で奇跡の実装を期待!
Pointer events 対応
マウス、タッチスクリーン、ペンなどのさまざまな入力を統合するべく策定されたポインターイベントがついにSafariに実装です
さらっと確認した感じ微妙な統合具合でしたが・・・近々↓の記事を更新したいと思います。
PCとスマホの Pointer Events 挙動まとめ - Qiita
- 投稿日:2019-09-09T10:52:45+09:00
課金なんて必要ない?YouTubeの広告自動スキップ&バナー自動削除のChrome拡張作ってみた
やっぱりYouTubeの広告、邪魔じゃない?
YouTubeプレミアムなるものがあって、課金すれば広告をなくすことができるようですが、そんなものにお金を払っている余裕はないのです。
それでも、ストレスレスにYouTubeを見たい!!普段、普通に鑑賞しているときはいいのですが、料理をしながらラジオ感覚でYouTubeを再生しているとき、手が汚れていて広告をスキップできない。
いちいちバナー広告が出てきて、字幕が読めない、などなど...そこで、自動で広告を飛ばしたり、バナー広告を消したりしてくれるChrome拡張があればいいやん!!ってことで、作ってみました。
とりあえず作ったもの紹介
※見づらくてすみません!
動画はプログラマー系YouTuberのKKBOYさんhttps://www.youtube.com/channel/UCEj6hquMBUiQGunwIO1zVZA
です。
動画のキャプチャ&Qiita、Twitter掲載はちゃんと許可をいただいてます!動画では動画の最初に流れる広告を飛ばしています。
ここでは写せていませんが、バナー広告も自動で消すことができます。ちゃんと Chrome拡張としてChromeに追加してあります。(アイコンも作りました。)
機能
・広告が流れたらすぐさま飛ばす
・バナーが現れたらすぐさま消すやること
- JavaScriptで機能実装
- アイコン作成
- JSONで設定ファイル作成
- Chromeに追加
実装について
まずはJavaScriptで機能実装
まずはソースコードをご覧ください。
youtube_auto.skip.jsconst host_url = location.host; if (host_url === "www.youtube.com") { setInterval( function () { try { const elem1 = document.getElementsByClassName("ytp-ad-overlay-close-container"); elem1[0].click(); } catch (e) { ; } try { const elem2 = document.getElementsByClassName("ytp-ad-skip-button-container"); elem2[0].click(); } catch (e) { ; } }, 1000 ) }仕様説明
URLからホストを特定して、YouTubeの時だけ動きます。
毎秒ページを監視して、動画広告やバナー広告を発見したら問答無用で消しに行きます。技術説明
初めてまともにJavascriptに触ったレベルなので、むしろアドバイスくださいって感じですが、念の為。
setInterval(第一引数, 第二引数)ってのは、定期的に処理を行う関数みたいです。
第一引数に定期的に行いたい処理、第二引数に定期的に行うそのインターバルの時間を設定します。
今回は『広告やバナーを消す』という処理を『1秒毎』に行いたいので、setInterval(function(), 1000)としています。
functionの中身は、『動画広告を消す』処理と『バナーを消す』処理になっています。
document.getElementsByClassName()というのは、クラス名を指定して、要素を配列で取得できる関数です。動画広告の『広告を飛ばすボタン』のクラス名とバナー広告のバツボタンを取得しています。
そして、その要素に対して.Click()を行うことで、要素に対して直接クリックイベントを起こします。これはこのアプリを作った後に気づいたのですが、YouTubeの動画広告はスキップボタンの上に『5秒後にスキップ』などの要素を被せてクリックできないようにしているようで、今回のアプリではそのようなクリックできないカバー要素を貫通してスキップボタンをクリックできます(下の要素に直接作用するため)。
追記
try~cacthを使っている理由としては、動画再生中も絶え間なくこのスクリプトが1秒おきに実行されていて、もちろんその間には動画広告やバナー広告がない時間もありますから、そのようなタイミングで実行した場合本来消すべき要素が見つからず1秒ごとにエラーを吐いてしまいます。
したがって、基本的に何もしなくていい場合はcatchでエラーを受け止めてあげます。今回if文を使ってホスト名による分岐処理を行なっているのは、後々他の動画サイトやその他webサイトの広告をにも対応させるためです。
サイトによって、消すべき要素を指すクラス名が違うと思われるので、どのようなサイトにも対応できるような仕様になっています。
YouTubeに限れば、ここの分岐処理ではなく、後述するmanifest.jsonでの設定を行うと良さそうです。Chrome拡張として公開したい!
ここまでしっかりとした機能を実装できると、Chrome拡張として世に出したい気分になります。
そこで、実際に公開するまでの手順も軽くシェアしておきたいと思います。(今はまだ審査中で世に出てはおりません...)JSONで設定ファイルを作る
Javascriptのプログラムファイルとは別に、その拡張機能についての設定ファイルを作る必要があるみたいです。
manifest.json{ "name": "YouAd Skipper", "version": "1.0.0", "manifest_version": 2, "description": "Sample Chrome Extension", "content_scripts": [{ "matches": [ "http://*/*", "https://*/*" ], "js": ["youtube_auto_skip.js"] }], "icons": { "16": "icon16.png", "48": "icon48.png", "128": "icon128.png" }, "permissions": ["activeTab"] }このファイルには、アイコン画像の指定、実際に動かすプログラムファイルの指定、拡張機能の名前などを設定します。
これがないとChromeには登録できません。アイコン画像について
アイコン画像はフリーのアイコンと文字をパワポで組み合わせて適当に作りました。
拡張機能公開について
結構めんどくさかったです。
アップロードするアイコン画像や宣伝用の画像はサイズが指定されていたり、初期登録料として5.00$取られたり...。
いつかこの5ドルを取り返すべく、何かサービスを作ろうと思います。審査を通過し公開でき次第、リンクを貼るか記事を書くかしたいと思います!
審査は3〜4日かかるみたいで(さっき手続きした)、公開にはしばらくかかるみたいです...。
(YouTube広告を消す拡張機能をGoogleさん、許してくれるかな...)Twitterの方も、いいね&フォロー&拡散お願いします!
YouTubeの動画広告、バナー広告を自動で飛ばしたり消したりするChrome拡張作りました!
— ひろすぐ (@lunain84) 2019年9月9日
いまChromeストアに登録中で、審査が通ったらリンク貼るのでぜひ使ってください!
動画はプログラマーYouTuberのkboyさんhttps://t.co/CRJnakD2Xw
許可取ってます。
技術的なこと↓https://t.co/d9m76iltxw pic.twitter.com/fcEetbcfmgとりあえず拡張を試してみたい方へ
TwitterでDMを送っていただければ、ファイル一式お送りいたします。
Chromeストアに登録されていない拡張機能もローカルから読み込むことができます。設定手順
- 投稿日:2019-09-09T10:41:11+09:00
RESTfulな「axios-mock-server」の使い方
axios-mock-serverとは
フロントエンドの開発体験を向上させ、生産性を格段に高めるために作られたTypeScript製のモックサーバーです。
axios専用ではあるものの、JSON Serverよりも遥かに手軽にRESTfulなモック環境を構築出来ます。
- GET/POST/PUT/DELETEのAPIエンドポイントを数行で作成できる
- サーバーを立てないので静的なJSファイルとしてSPA上でも動かせる
- TypeScript対応
- Node環境のaxiosでも動く
- Nuxt.js同様のオートルーティング機能でパス記述が不要
開発の背景
JS製のモックとしてデファクトスタンダードっぽいJSON Serverが使いづらいなーと思ったのがきっかけ。
GETだけなら簡単なんだけど、POSTやDELETEをデータに反映させようとするとExpressサーバー丸ごと書くのと同じような作業が必要で気軽には扱えない。そこで、昨年末くらいから私が設計したプロジェクトそれぞれにカスタムして組み込んでいたaxios専用のモックサーバーをOSSとして公開しました。
axios-mock-adapterとNuxt.jsから実装のヒントを得ています。使い方 (JavaScript + ES6 modules)
インストール
$ npm install --save-dev axios-mock-serverAPI作成
Nodeプロジェクトのルートに
mocksディレクトリを作成します。
Nuxt.jsのオートルーティングと同じ命名規則でjsファイルを作成します。
以下の例だと/v1/users/${userId}のエンドポイントにGETとPOSTのモックAPIが作成されます。
メソッドが受け取る引数は{ values, data, config }(valuesはパスのアンダースコア部分、dataはPOSTなどで送信したデータ、configはaxiosのリクエストconfig)で、返り値は[HTTPステータス, データ, ヘッダー]です。(必須要素はHTTPステータスのみ)
async/awaitも使用可能です。mocks/v1/users/_userId.jsconst users = [ { id: 0, name: 'taro' }, { id: 1, name: 'hanako' } ] export default { get({ values }) { return [200, users.find(user => user.id === values.userId)] }, post({ data }) { users.push({ id: users.length, name: data.name }) return [201] } }axiosとの接続
npmスクリプトでビルドすると
mocks/$route.jsが生成されます。
以下の方法でaxiosの接続先がモックサーバーに変わります。src/index.jsimport axios from 'axios' import mockServer from 'axios-mock-server' import route from '../mocks/$route' mockServer(route) axios.get('https://google.com/v1/users/1').then((user) => { console.log(user) // { id: 1, name: 'hanako' } })axiosのinstanceをモックしたい場合は初期化時に引数で渡します。
src/index.jsimport axios from 'axios' import mockServer from 'axios-mock-server' import route from '../mocks/$route' const client = axios.create({ baseURL: 'https://google.com/v1' }) mockServer(route, client) client.get('/users/1').then((user) => { console.log(user) // { id: 1, name: 'hanako' } }) axios.get('https://google.com/v1/users/1').catch((e) => { console.log(e.response.status) // 404 (axios本体はモックされてない) })npmスクリプト
mocks/$route.jsを1回ビルドするのが-b
ファイルが変更されるたびにビルドするのが-w
設定ファイルの場所を変えたい場合は-c <file path>package.json{ "scripts": { "mock:build": "axios-mock-server -b", "mock:watch": "axios-mock-server -w", "mock:config": "axios-mock-server -b -c settings/.mockserverrc" } }NeDBで永続化
DBガッツリ使うとモックの意義が薄れるのですが、とはいえデータの変更を保持しておきたい場面もあるわけです。
そこでNeDBというJavaScriptのみで書かれたDBを紹介します。
MongoDBライクにテーブル定義や設定せずにすぐ使えるので今回のような用途にピッタリです。
静的ファイルだけのSPAでも動作するのが素晴らしい・・・動作環境に応じて最適なデータの保存先を自動決定してくれます。
Node.jsならファイルに、ブラウザならIndexedDB、なければlocalStorageになるようです。
とはいえNeDBは非同期メソッドがコールバック方式で扱いづらいのでラッパーライブラリのNeDB-promisesを使うのが良さそうです。
(単数形のNeDB-promiseは更新が止まっているのとTSの型定義ファイルがない別物ので注意)NeDB-promisesインストール
$ npm install --save-dev nedb-promisesNeDB-promises + JavaScript
mocks/v1/users/_userId.jsimport Datastore from 'nedb-promises' const datastore = Datastore.create('dbname') export default { async get({ values }) { return [200, await datastore.find({ id: values.userId })] }, async post({ values, data }) { return [ 201, await datastore.insert({ id: values.userId, name: data.name }) ] } } // 以下のように書いても等価です // asyncData(HTTPステータス, データを返すPromise, ヘッダー) import { asyncResponse } from 'axios-mock-server' import Datastore from 'nedb-promises' const datastore = Datastore.create('dbname') export default { get: ({ values }) => asyncResponse(200, datastore.find({ id: values.userId })), post: ({ values, data }) => asyncResponse(201, datastore.insert({ id: values.userId, name: data.name })) }multipart-formdata対応
サーバーを立てずにモックしているため、通常の方法では画像をPOSTしたあとにimgタグで表示する方法がありません。
AWS S3など外部に保存するのも手間がかかりすぎるので、ここではdataURIを使う方法を紹介します。mocks/v1/images/index.jsexport const images = [] export default { post: ({ data }) => new Promise((resolve) => { const file = data.get('file') // FormData#get const reader = new FileReader() reader.onload = () => { const image = { id: images.length, url: reader.result } images.push(image) resolve([200, image]) } reader.readAsDataURL(file) }) }mocks/v1/images/_imageId.jsimport { images } from './index' export default { get({ values }) { return [200, images.find(image => image.id === values.imageId)] } }src/index.jsconst inputElm = document.getElementsByTagName('input')[0] inputElm.addEventListener('change', async (e) => { const formData = new FormData() formData.append('file', e.target.files[0]) const { data: { id }} = await axios.post('/v1/images', formData, { headers: { 'content-type': 'multipart/form-data' } }) const { data: { url }} = await axios.get(`/v1/images/${id}`) console.log(url) // data:image/jpg;base64,.. const img = new Image() img.src = url document.body.appendChild(img) }, false)@nuxtjs/axiosとの連携
Axios Moduleのセットアップが完了している前提で解説します。
create-nuxt-appを使うとaxios込みでNuxt.jsのインストールがラクです。plugins/mock.jsimport mockServer from 'axios-mock-server' import route from '~/mocks/$route' export default ({ app }) => { mockServer(route, app.$axios) }nuxt.config.jsexport default { plugins: ['~/plugins/mock.js'] }mocks/users/_userId.jsconst users = [ { id: 0, name: 'taro' }, { id: 1, name: 'hanako' } ] export default { get({ values }) { return [200, users.find(user => user.id === values.userId)] } }pages/index.vue<template> <div /> </template> <script> export default { async mounted() { console.log( await this.$axios.$get('/users/1') ) // { id: 1, name: 'hanako' } } } </script>package.json{ "scripts": { "dev": "axios-mock-server -w | nuxt", "build": "axios-mock-server -b & nuxt build", "start": "axios-mock-server -b & nuxt start", "generate": "axios-mock-server -b & nuxt generate" }, "dependencies": { "@nuxtjs/axios": "^5.3.6", "axios-mock-server": "^0.8.1", "nuxt": "^2.0.0" } }レスポンス時間を遅延
デフォルト設定だとレスポンスは非同期ではあるものの即座に返されます。
ネットワークの遅延をシミュレートしたい場合はsetDelayTimeを使います。src/index.jsimport axios from 'axios' import mockServer from 'axios-mock-server' import route from '../mocks/$route' mockServer(route).setDelayTime(500) // ms console.time() axios.get('/v1/users/1').then(() => { console.timeEnd() // default: 501.565185546875ms })使い方 (TypeScript + ES6 modules)
設定ファイル
Nodeプロジェクトのルートに
.mockserverrcという名前の設定ファイルを作成。
outputExtでビルド時に生成されるmocks/$route.jsの拡張子をtsに変更します。.mockserverrc{ "outputExt": "ts" }mocks/v1/users/_userId.tsimport { MockMethods, MockResponse } from 'axios-mock-server' export type User = { id: number, name: string } const users: User[] = [ { id: 0, name: 'taro' }, { id: 1, name: 'hanako' } ] const methods: MockMethods = { async get({ values }) { return [200, users.find(user => user.id === values.userId)] as MockResponse } } export default methodssrc/index.tsimport axios from 'axios' import mockServer from 'axios-mock-server' import route from '../mocks/$route' import { User } from '../mocks/v1/users/_userId' mockServer(route) axios.get<User>('https://google.com/v1/users/1').then((user) => { console.log(user) // { id: 1, name: 'hanako' } })使い方 (Node.js + CommonJS modules)
設定ファイルで
targetをcjsに変更し、CommonJS形式でexportします。(デフォルトはes6).mockserverrc{ "target": "cjs" }mocks/v1/users/_userId.jsconst users = [ { id: 0, name: 'taro' }, { id: 1, name: 'hanako' } ] module.exports = { get({ values }) { return [200, users.find(user => user.id === values.userId)] } }src/index.jsconst axios = require('axios') const mockServer = require('axios-mock-server') const route = require('../mocks/$route') mockServer(route) axios.get('https://google.com/v1/users/1').then(function(user) { console.log(user) // { id: 1, name: 'hanako' } })まとめ
サーバーの実装を待たずにフロントが先行して開発を進めるとか、フロントチームだけでプロトタイプを作るのにも相当役立つはずです。
HTTPクライアント界隈では最近kyがアツい感じなのでaxiosがいつまでスタンダードであり続けるかは気になるところですが。バグ報告や使い方の質問は気軽にいただけると嬉しいです。
- 投稿日:2019-09-09T08:19:49+09:00
名前空間でブックマークを作成
spaceBookMarks (github) (chrome拡張)
これは何
- 「ブックマーク作成に一々マウス使いたくねぇ」 そんな方のために作成中の、キーボードのみでブックマークが作成できるブラウザプラグインです。
特徴
blog::YSawcと入力してEnterを押下すると、blogファイルを検索して、直下にブックマーク登録できます。
- 詳細: 名前空間でテキストを入力すると、
::を元にしてファイルディレクトリして指定でき、そのファイル直下にブックマークが作成されます。
undo
- 直前に作成したブックマーク、やっぱ要らないから削除したい、ってなることありませんか?ありますよね?!?で、どこにディレクトリ作ったのかマウスでディレクトリを辿っていくのですが、フォルダが少し深くなると段々イライラしてきます。そんなに気に
undoです。直前の作成したブックマークを削除出来ます。テキスト部分にコマンドを打ち込むことでブックマークの作成、間違った時用に
undo、undoを取り消すredoの機能も備えました。..以上(現在進行系)
不親切な仕様です (TODO: 実装)
- UIはかなり見辛いかもです。タイトルとテキストボックスしかなく、コマンドを打ち込んでenterを押下しても、見た目が変わることがないのですが仕様です。コマンドを連打しないように気をつけてください。
- 例として、blog1のフォルダの中のafterReadのフォルダの中にhogeというブックマークを作りたいとして、
blog1::afterRead::hogeという使い方は現在実装出来ていません。
- 対策というか仕様
- ブックマークしたい直上のディレクトリ名を
::の前に指定して使用してください。(blog直下に作りたいならblog::hogeみたいなイメージです。更に例を上げると、DIR1/DIR2/DIR3/.../DIR99/[ここにブックマークファイル作りたい]、であればDIR99::HOGEといった使い方です。)- オプションでディレクトリを辿れる実装をする予定です。
- あとは現時点だとディレクトリ・ファイル名を区別させていないので、名前がかぶっていると思ったような挙動にならないです。
undoは連打すると大変なことになっていきます。その場合redoを適当回数連打してください。redoは連打すると大変なことになっていきます。その場合undoを適当回数連打してください。まだ未実装 (思いつき次第追記します。)
- 後で足していきます。
仕様周り軽く解説
blog::YSawcblogという名前のディレクトリを検索して、直下にYSawcというタイトル、現在開いているタブのURLでブックマークを保存します。
- ディレクトリの親ノード
デフォルトだとブラウザ標準の[ブックマーク]というディレクトリが親ノードになります。
なので、ユーザー任意でディレクトリを指定できるように実装予定です。最後に (私事)
javascriptに入門して2日間くらいで大枠を作ったので、中身の実装部分は相当の荒れ模様です。
なので、リファクタリングしていく楽しみがあります。(あと個人的にメインなタスクとしてtypescriptに移行するのも超楽しみ)
chromeの拡張用の絵をまだ書いてないので、chromeの拡張絵自体はフリー素材を使ってます。機能拡大はそこまでしないと思います。実装するとしたら、ミニマムに実装していきたいです。
- 投稿日:2019-09-09T08:19:49+09:00
キーボードでブックマークを作成
spaceBookMarks (github) (chrome拡張)
これは何
- 「ブックマーク作成に一々マウス使いたくねぇ」 そんな方のために作成中の、キーボードのみでブックマークが作成できるブラウザプラグインです。
特徴
blog::YSawcと入力してEnterを押下すると、blogファイルを検索して、直下にブックマーク登録できます。
- 詳細: 名前空間でテキストを入力すると、
::を元にしてファイルディレクトリして指定でき、そのファイル直下にブックマークが作成されます。
undo
- 直前に作成したブックマーク、やっぱ要らないから削除したい、ってなることありませんか?ありますよね?!?で、どこにディレクトリ作ったのかマウスでディレクトリを辿っていくのですが、フォルダが少し深くなると段々イライラしてきます。そんなに気に
undoです。直前の作成したブックマークを削除出来ます。テキスト部分にコマンドを打ち込むことでブックマークの作成、間違った時用に
undo、undoを取り消すredoの機能も備えました。..以上(現在進行系)
不親切な仕様です (TODO: 実装)
- UIはかなり見辛いかもです。タイトルとテキストボックスしかなく、コマンドを打ち込んでenterを押下しても、見た目が変わることがないのですが仕様です。コマンドを連打しないように気をつけてください。
- 例として、blog1のフォルダの中のafterReadのフォルダの中にhogeというブックマークを作りたいとして、
blog1::afterRead::hogeという使い方は現在実装出来ていません。
- 対策というか仕様
- ブックマークしたい直上のディレクトリ名を
::の前に指定して使用してください。(blog直下に作りたいならblog::hogeみたいなイメージです。更に例を上げると、DIR1/DIR2/DIR3/.../DIR99/[ここにブックマークファイル作りたい]、であればDIR99::HOGEといった使い方です。)- オプションでディレクトリを辿れる実装をする予定です。
- あとは現時点の仕様で、同名のディレクトリがあると思ったような挙動にならないです。
undoは連打すると大変なことになっていきます。その場合redoを適当回数連打してください。redoは連打すると大変なことになっていきます。その場合undoを適当回数連打してください。まだ未実装 (思いつき次第追記します。)
- 後で足していきます。
仕様周り軽く解説
blog::YSawcblogという名前のディレクトリを検索して、直下にYSawcというタイトル、現在開いているタブのURLでブックマークを保存します。
- ディレクトリの親ノード
デフォルトだとブラウザ標準の[ブックマーク]というディレクトリが親ノードになります。
なので、ユーザー任意でディレクトリを指定できるように実装予定です。最後に (私事)
javascriptに入門して2日間くらいで大枠を作ったので、中身の実装部分は相当の荒れ模様です。
なので、リファクタリングしていく楽しみがあります。(あと個人的にメインなタスクとしてtypescriptに移行するのも超楽しみ)
chromeの拡張用の絵をまだ書いてないので、chromeの拡張絵自体はフリー素材を使ってます。機能拡大はそこまでしないと思います。実装するとしたら、ミニマムに実装していきたいです。
- 投稿日:2019-09-09T08:19:49+09:00
キーボードでブックマークを作成 chrome拡張宣伝
spaceBookMarks (github) (chrome拡張)
これは何
- 「ブックマーク作成に一々マウス使いたくねぇ」 そんな方のために作成中の、キーボードのみでブックマークが作成できるブラウザプラグインです。
特徴
blog::YSawcと入力してEnterを押下すると、blogファイルを検索して、直下にブックマーク登録できます。
- 詳細: 名前空間でテキストを入力すると、
::を元にしてファイルディレクトリして指定でき、そのファイル直下にブックマークが作成されます。
undo
- 直前に作成したブックマーク、やっぱ要らないから削除したい、ってなることありませんか?ありますよね?!?で、どこにディレクトリ作ったのかマウスでディレクトリを辿っていくのですが、フォルダが少し深くなると段々イライラしてきます。そんなに気に
undoです。直前の作成したブックマークを削除出来ます。テキスト部分にコマンドを打ち込むことでブックマークの作成、間違った時用に
undo、undoを取り消すredoの機能も備えました。..以上(現在進行系)
不親切な仕様です (TODO: 実装)
ショートカット設定がまだ実装出来ていないので、起動にはマウスを使わないと行けない。(TODO: 優先度高)
UIはかなり見辛いかもです。タイトルとテキストボックスしかなく、コマンドを打ち込んでenterを押下しても、見た目が変わることがないのですが仕様です。コマンドを連打しないように気をつけてください。
例として、blog1のフォルダの中のafterReadのフォルダの中にhogeというブックマークを作りたいとして、
blog1::afterRead::hogeという使い方は現在実装出来ていません。
- 対策というか仕様
- ブックマークしたい直上のディレクトリ名を
::の前に指定して使用してください。(blog直下に作りたいならblog::hogeみたいなイメージです。更に例を上げると、DIR1/DIR2/DIR3/.../DIR99/[ここにブックマークファイル作りたい]、であればDIR99::HOGEといった使い方です。)- オプションでディレクトリを辿れる実装をする予定です。
あとは現時点の仕様で、同名のディレクトリがあると思ったような挙動にならないです。
undoは連打すると大変なことになっていきます。その場合redoを適当回数連打してください。
redoは連打すると大変なことになっていきます。その場合undoを適当回数連打してください。まだ未実装 (思いつき次第追記します。)
- 後で足していきます。
仕様周り軽く解説
blog::YSawcblogという名前のディレクトリを検索して、直下にYSawcというタイトル、現在開いているタブのURLでブックマークを保存します。
- ディレクトリの親ノード
デフォルトだとブラウザ標準の[ブックマーク]というディレクトリが親ノードになります。
なので、ユーザー任意でディレクトリを指定できるように実装予定です。最後に (私事)
javascriptに入門して2日間くらいで大枠を作ったので、中身の実装部分は相当の荒れ模様です。
なので、リファクタリングしていく楽しみがあります。(あと個人的にメインなタスクとしてtypescriptに移行するのも超楽しみ)
chromeの拡張用の絵をまだ書いてないので、chromeの拡張絵自体はフリー素材を使ってます。機能拡大はそこまでしないと思います。実装するとしたら、ミニマムに実装していきたいです。
- 投稿日:2019-09-09T06:32:01+09:00
React Native + Expo アプリで unstatedのStateにaxiosを使って簡易テストサーバーからデータ取得
この記事は、「【連載】初めてのReact Native + Expo開発環境構築入門」の子記事です。環境などの条件は、親記事をご覧ください。
前回までに、unstatedに格納した請求書情報をきれいに画面上にリスト表示できるようになったので、今回は請求書情報をサーバーから取得できるようにします。テスト用に簡易ローカルHTTPサーバーも立ち上げます。
サーバーとの通信は、Reduxよりunstatedが楽
Reduxを使った場合、そもそもそのままではサーバーからデータを取得してグローバルStateに取得データを格納できません。(厳密にはできますが、各コンポーネント内に非同期処理を書かなければいけないので、コンポーネント間でコード再利用ができない。)なので通常redux-thunkというミドルウェアを使って
Actionで非同期処理を実行できるようにします。これに比べて、unstatedなら2つの点で断然有利です。
- ミドルウェア無しで(追加モジュール無しで)非同期処理できる
- Containerのメンバーメソッドとして非同期処理を書ける。ActionやReducerなどあちこちに処理を書かなくていい
※もちろんReduxのほうがいい場合もあります。
テストサーバーを立てる
実際のコーディングの前に、まずはテストサーバーを立てましょう。ローカルPCにnode.jsのサーバーを立てて、そこにinvoice.jsというファイルを置いて、HTTP通信で取得できるようにします。今回はnode.jsのサーバーを簡易的に立ててくれるhttp-serverを使います。
まず
http-serverをグローバルインストール。npm install -g http-server次に、テストサーバーのドキュメントルートフォルダを設置。プロジェクトフォルダの横とかがわかりやすいかもですね。
たとえば今回のプロジェクトは私の場合C:\ExpoProjects\hello-world-testに設置しているので、C:\ExpoProjects\hello-world-testdataというフォルダを作って、以下のinovice.jsを設置します。invoice.js{"customers":[{"id":0,"name":"ABC Store","addr1":"123 Abc St.","addr2":"","city":"New York","state":"NY","zip":"10001"},{"id":1,"name":"123 Deli","addr1":"1 Def Ave.","addr2":"","city":"New York","state":"NY","zip":"10002"},{"id":2,"name":"Xyz mart","addr1":"23 Xyz Blvd.","addr2":"","city":"New York","state":"NY","zip":"10003"},{"id":3,"name":"Xyz2 mart","addr1":"1 Xyz Blvd.","addr2":"","city":"New York","state":"NY","zip":"10004"}],"products":[{"id":0,"name":"Blue ribbon","shortName":"B.R.","price":10.5,"cost":7.2},{"id":1,"name":"Red ribbon","shortName":"R.R.","price":9.5,"cost":6},{"id":3,"name":"White shirt","shortName":"W.S.","price":15,"cost":9.3}],"invoices":[{"id":0,"date":"2/2/2019","customer":0,"items":[{"product":0,"qty":5,"adjust":0,"credit":0},{"product":1,"qty":3,"adjust":0,"credit":0},{"product":2,"qty":4,"adjust":0,"credit":0}]},{"id":1,"date":"2/2/2019","customer":0,"items":[{"product":0,"qty":7,"adjust":0,"credit":0}]},{"id":2,"date":"2/2/2019","customer":3,"items":[{"product":0,"qty":5,"adjust":0,"credit":0},{"product":1,"qty":3,"adjust":0,"credit":0},{"product":2,"qty":4,"adjust":0,"credit":0}]},{"id":3,"date":"2/2/2019","customer":2,"items":[{"product":0,"qty":5,"adjust":0,"credit":0},{"product":1,"qty":3,"adjust":0,"credit":0},{"product":2,"qty":4,"adjust":0,"credit":0}]}]}長いのでコンパクト化していますが、中身が気になる方は、オンラインJSONエディタに貼り付けてみてください。このデータにはInvoiceが4つ入っています。
ではローカルサーバーを立てます。PowerShellを開き(Visual Studio Codeの中ではなく、独立したPowerShellを立ち上げます)、ドキュメントルートまで移動して
http-serverを実行。cd \ExpoProjects\hello-world-testdata http-server上のようにどこに立ち上がってるか表示してくれるので、実験に使ってるモバイル実機のブラウザでアクセスしてみます。このとき、ポート番号とファイル名を忘れないように注意します。
axiosをインストール
axiosはHTTP通信を「よしなに」処理してくれる便利屋さんです。なくてもいいですが、記述が簡単になるのでぜひ。
npm install axiosunstatedコンテナに通信用メソッドを追加
以前の記事で作ったunstatedコンテナ
containers/InvoiceContainer.jsに、API通信を実行するメソッドを追加します。/containers/InvoiceContainer.jsimport axios from 'axios'; const INVOICE_API_ENDPOINT = 'http://192.168.1.8:8080/invoice.js'; ... export default class InvoiceContainer extends Container { constructor(props = {}) { super(); this.state = { data: props.initialSeeding ? Seeder.getSeed() : this.getEmptyData(), isDataLoading: false }; } ... getDataFromServer() { this.setState({ isDataLoading: true }); axios .get(INVOICE_API_ENDPOINT, { params: {} }) .then(results => { console.log("HTTP Request succeeded."); console.log(results); this.setState({ data: results.data }); this.setState({ isDataLoading: false }); }) .catch(() => { console.log("HTTP Request failed."); this.setState({ isDataLoading: false }); }); } ... }
INVOICE_API_ENDPOINTには、簡易テストサーバーのアドレスに対象データファイル名まで入れたURLを指定します。グローバルStateとして、
isDataLoadingを追加しました。axiosがサーバーと通信中にこれをオンにすることで、画面コンポーネント側でこのフラグを見て表示内容を変更できることを狙っています。追加した
getDataFromServer()では、axiosによるサーバーとの通信が実装されています。最初にロード中フラグisDataLoadingを立てて、INVOICE_API_ENDPOINTと通信し、成功(.then)したらグローバルStateのdataに書き込んでからロード中フラグオフ、失敗(.catch)したら単にロード中フラグオフ。これだけで通信コーディングは終わりです。実践ではエラーになった場合の処理についてもう少し書く必要がありますね。
API通信を開始するボタンを作る
単純に、画面コンポーネントに
Importボタンとイベントを実装します。HomeScreen.jsclass HomeScreenContent extends React.Component { constructor(props) { super(props); this.onImportClick = this.onImportClick.bind(this); } onImportClick() { this.props.globalState.getDataFromServer(); } ... render() { let globalState = this.props.globalState; let invoiceList = <Text>No invoice</Text>; // Wait for data loading... if (globalState.state.isDataLoading) { return ( <View> <Text>loading...</Text> </View> ); } ... return ( <Container> <Content> <View style={{ flexDirection: "row" }}> <Left> <Button light style={{ justifyContent: "flex-start" }} onPress={() => this.onImportClick()}> <Icon type="FontAwesome5" name="file-import" /> <Text style={{ paddingLeft: 0 }}>Import</Text> </Button> </Left> <Right> <Button style={{ justifyContent: "flex-end" }} onPress={() => this.props.navigation.navigate("Summary")}> <Icon type="FontAwesome5" name="poll-h" /> <Text style={{ paddingLeft: 0 }}>Summary</Text> </Button> </Right> </View> {invoiceList} <Button style={{ justifyContent: "flex-start" }} onPress={() => this.props.navigation.navigate("InvoiceEdit")}> <Icon type="FontAwesome5" name="file-invoice-dollar" /> <Text style={{ paddingLeft: 0 }}>InvoiceEdit</Text> </Button> </Content> </Container> ); } } ...まずイベントハンドラ
onImportClick()を登録します。この中では、グローバルStateコンテナに書いたgetDataFromServerを呼んでるだけです。
次に、render()内に、通信中(グローバルStateのisDataLoadingがオン)だったら「loading...」と表示するコードを入れます。これにより、通信中にほかの操作をされることを防ぎます。
最後に、Importボタンを追加します。タップされたらthis.onImportClick()を呼ぶだけです。
※さらに若干ボタンの配置を変えています。修正が終わったら、テスト用サーバーがPowerShell上で起動していることを確認して、モバイル実機でテストします。結果は、以下のようになります。
いかがでしょうか。Redux+Thunk+axiosを使ったことがある方は、あまりの簡単さに驚きが隠せないのでは?私はよっぽど大きなプロジェクトでない限り、もうReduxに戻りたくないです。。。
- 投稿日:2019-09-09T02:57:16+09:00
WebComponentsのHTMLImportsのグローバル汚染の悩みをすべて解決してみた
index.html<html> <head> <link rel="import" href="/components/hello-world.html?as=hoge-piyo"> </head> <body> <hello-world>動かない</hello-world> <hoge-piyo>動く</hoge-piyo> </body> </html>components/hello-world.html<template> <div id="border-div"> <p>Hello World!!!</p> <slot></slot> </div> </template> <script> /* # このコードの素晴らしい点 ## A. グローバル汚染をしていないこと 正確にはcustomElementの名前を一つ取っているが、それは利用側でコントロール可能(後述)なので、汚染とは言わない。esmoduleと同じ。 本当に何一つ汚染していない。クラス定義ですらも! ## B. 利用側のimportの仕方に依存していない ownerDocumentを用いているので、利用側で、どのDomにHTMLImportされているのか、Component側は意識していない ## C. コンポーネント名を強制させない デフォルトのコンポーネント名としてhello-worldを提供しているがそれだけである。 利用側が自由に名前をつけることができる。 他にも同盟のコンポーネントがあっても全く問題がない */ { const ownerDocument = document.currentScript.ownerDocument; const moduleName = new URL(document.currentScript.ownerDocument.URL).searchParams.get('as') || "hellow-world" window.customElements.define(moduleName, class extends HTMLElement { constructor() { super() const shadowRoot = this.attachShadow({ mode: 'open' }) const t = ownerDocument.getElementsByTagName('template')[0]; const instance = t.content.cloneNode(true); shadowRoot.appendChild(instance); } }); } </script>HTMLImportsを使ったときの悩み
- customElement名が被ったら(同名のコンポーネントがあったら)死ぬ
- → importじのURLパラメータでcomponent名を渡すことで解決
- scriptからtemplateを参照するのに、documentから探索するのでは、コンポーネントに利用側の都合が発生して辛い
- → ownderDocumentを用いることで解決
- ownerDocumentはコールバック(constructorのこと)では使えないので、定数に格納する必要があるが、また一つグローバル汚染が増える
- esnextのスコープ構文でくるむことで解決
- ownerDocument等をスコープに隔離するとクラスで読めないし、クラスもスコープに入れると外部から参照できないジレンマ
- → クラスを外部公開せず、直接customElement.defineにわたすことで解決
- クラス定義自体が一つグローバル汚染になる
- → クラスを外部公開せず、直接customElement.defineにわたすことで解決
感想
HTMLImportsの辛い部分をほぼ払拭できたのではないだろうか。
私が見た記事の中で、この自分のコードが一番筋が良いと思う。
全世界に広まり、HtmlImports自体も復権することを願う。1
本当に残念なことに、HTMLImportsはWebComponentsの中で非推奨になってしまい、今はesModuleで読み込むのが主流になっているみたいだ。最悪である。なぜなら、jsの中でhtmlをベタ書きしなくてはならないから。WebComponentsがただの改悪版Reactに成り下がったのである。(Mozillaが実装しなかったかららしい?)ともかくいくら残念とはいえHTMLImportsはスタンダードではない。 ↩
- 投稿日:2019-09-09T02:36:49+09:00
5秒で出来るtinder自動右スワイプ
手順1
PCでGoogle Chromeを開く
手順2
パソコン版ティンダーにログイン
https://tinder.com/手順3
スワイプする画面で
右クリックで検証を開く手順4
手順5
auto = setInterval( function(){ var elem= document.getElementsByClassName("recsGamepad__button"); elem[3].click() },500)上記のコードをコピペし、
Enterキーを押す。
超簡単5stepで
プログラマーじゃなくても自動右スワイプが可能に!!
- 投稿日:2019-09-09T00:37:40+09:00
ゴミの日も記念日も何でも通知してくれるLINE BotをGASで作ってみよう
LINE Botをリマインダ的に活用して生活に役立てるのが流行っていますね。特に、LINE BotのMessaging APIとGoogle Apps Script(通称GAS)との相性が良くて、とても簡単にBotが作れるみたいです。
本記事では、LINE Messaging APIとGASを使って、何でも通知してくれる便利なLINE Botを作ってみたいと思います。
関連する記事との比較
ゴミの種類を通知するLINE Botは、既にいくつか作ってみた記事があります。やはり分別は面倒ですね。
これらの記事では、ゴミの日種類と曜日をコード内で定義しています。例えば、引っ越しをしてゴミの曜日や分別方法が変わった場合、コードを変更する必要があります。本記事では、通知する内容とコードを切り離し、コードがわからない人でも、通知する日時と内容を編集できることを目指します。
設計
以下の3つを利用して、何でも通知してくれるLINE Botを実装します。
- LINE Messaging API
- Google スプレッドシート
- Google Apps Script
LINE Messaging API の準備
通知する内容を喋ってくれるLINE Botのアカウントを作成します。これに関しては、わかりやすく説明してくれる記事が沢山あるので、それに従ってください。
LINE BOTの作り方を世界一わかりやすく解説(1)【アカウント準備編】
アカウントが準備できたら、「チャネル基本設定」>「メッセージ送受信設定」から、「アクセストークン
(ロングターム)」を発行してください。これを後に利用します。スプレッドシートの準備
Google スプレッドシートを新規作成し、このような形式の表を作成してください。
表の構成を見ていただければわかるかと思いますが、以下のことが実現できるようになります。
- 日時や曜日を指定して、狙ったタイミングで通知ができる
- 毎分、毎時、毎日のように、定期的な通知をしたい場合は「*」を入力することで実現可能
- 第1月曜日、第2火曜日のように、何番目の何曜日という指定も可能(ゴミ分別の通知に役立ちます!!)
- 「発言する場所」を指定することで、通知内容ごとに別々の場所で発言できる
※本記事では、「発言する場所」に入力するIDを調べるためのBotの機能も同時に実装します。
シートのサンプルも用意したので、適宜利用してください。
注意点としては、
- 分、時、日、月、曜日、第n週、本文、発言する場所という列の並びを変えないこと
- シート名(スプレッドシートの画面下部)を「alarm」とすること
- いずれも変更する場合は後述するGASのコードを変更する必要があります。
Google Apps Scriptのコード
スプレッドシートの「ツール」>「スクリプトエディタ」で、Google Apps Scriptのエディタを開いてください。
以下のコードをエディタで記入してください。コードはGithubにも一応公開しています。
ここで、
ACCESS_TOKEN変数に、LINE Messaging APIの準備で準備した「アクセストークン(ロングターム)」を記載してください。
SHEET_KEYは、スプレッドシートのURLに含まれている文字列です。
https://docs.google.com/spreadsheets/d/{この部分の文字列です!}/edit#gid=0Code.gs// 「メッセージ送受信設定」のアクセストークン(ロングターム)を記入してください。 var ACCESS_TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; // 発言させたい日時と内容が書かれたシートのKeyを記入してください。 // シート名には「alarm」としてください。 var SHEET_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; function doPost(e) { var event = JSON.parse(e.postData.contents).events[0]; var userMessage = event.message.text; var message = ""; if ( userMessage === "ID" ) { message = tellID(event); } else { // 疎通確認が終わったらコメントアウトすると良いです。 message = "メッセージを受け取ったわよ!"; } replyMessage(event.replyToken, message); return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON); } function replyMessage(token, message) { UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', { 'headers': { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Bearer ' + ACCESS_TOKEN, }, 'method': 'post', 'payload': JSON.stringify({ 'replyToken': token, 'messages': [{ 'type': 'text', 'text': message, }], }), }); } function pushMessage(to, message) { UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', { 'headers': { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': 'Bearer ' + ACCESS_TOKEN, }, 'method': 'post', 'payload': JSON.stringify({ 'to': to, 'messages': [{ 'type': 'text', 'text': message, }], }), }); } function tellID(event) { // ID var userID = event.source.userId; var talkID = ""; if (event.source.type === "group") { talkID = event.source.groupId; } else if (event.source.type === "room") { talkID = event.source.roomId; } var message = "あなたのID: " + userID; if (talkID != "") { message += "\nこのチャットのID: " + talkID; } return message; } function notice() { var sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('alarm'); var data = sheet.getDataRange().getValues(); var dayStr = ["日", "月", "火", "水", "木", "金", "土"]; var now = new Date(); for (var i=1; i<data.length; i++) { var [minute, hour, dayOfMonth, month, dayOfWeek, weekNum, message, to] = data[i]; if ( ( minute == now.getMinutes() || minute === "*") && (hour == now.getHours() || hour === "*") && (dayOfMonth == now.getDate() || dayOfMonth === "*") && (month == now.getMonth() + 1 || month === "*") && (dayOfWeek === dayStr[now.getDay()] || dayOfWeek === "*") && (weekNum == parseInt(now.getDate() / 7) + 1 || weekNum === "*") ) { pushMessage(to, message); } } }疎通確認
コードが準備できたら、「ウェブアプリケーションとして導入」をしてください。
注意点としては、「アプリケーションにアクセスできるユーザ」を、「全員(匿名ユーザーを含む)」にすることです。
完了したら、ウェブアプリケーションのURLを、LINE Developerの「チャンネル基本設定」>「メッセージ送受信設定」>「Webhook URL」に入力します。「Webhook送信」も有効化してください。
「接続確認」に成功すればOKです。
この状態でBotに何か話かけてみましょう。
メッセージが返ってくればOKです。
発言先のIDを調べて通知を設定する
通知内容の発言先「発言する場所」に入力するIDを調べてみましょう。通知をさせたいトークルームにBotを招待して、「ID」と発言すればOKです。
頭文字がUのものが、個人のID、Rのものが、グループではないトークルームのID、Gのものが、グループのトークルームIDのようです。ここで調べたIDを、スプレッドシートの「発言する場所」に記入します。
トリガーを設定する
時間になったら通知をするためのトリガーを設定します。Google Apps Scriptのエディタから時計マークをクリックしてください。
トリガーを追加
実行する関数: notice
イベントのソース: 時間主導型
トリガーのタイプ: 分ベースのタイマー
時間の間隔: 1分おき
これで設定は一通り完了です。
シートに設定した通りの通知が来れば成功です!最後に
通知する内容、日時、通知先をGoogleスプレッドシートで指定可能なLINE Botを作成してみました。このBotを一度設定すれば、GASのコードがわからない家族や友達であっても、スプレッドシートの編集権限を与えて共有することで、各々が好きな内容の通知を好きなトークルームへ設定できるようになります。どこかで活用してくださる方が現れると嬉しいです。
記事を書いていて気が付きましたが、現在のコードでは、「本文」が空の場合や、「発言する場所」に不適切なIDが記入されていた場合にエラーが発生して、発生した箇所より下の通知が止まってしまうようです。気が向いたら対応します。
- 投稿日:2019-09-09T00:22:18+09:00
JavaScriptのプロトタイプ汚染攻撃対策は難しい
先日、私のプロジェクトで脆弱性関連のissueが投稿されたので対策を行いました。
指摘内容は主に「プロトタイプ汚染攻撃」でした。自分では対策を行っていたつもりだったのですが、様々な穴がありました。
プロトタイプ汚染攻撃可能な脆弱性は成功すると他の機能や脆弱性との組み合わせによって、任意のコード実行を可能にする危険度の高いものですが、XSSやCSRFに比べて、初学者が触れられる纏まった対策方法の情報が少ないと感じたので、ここに記そうと思います。プロトタイプ汚染攻撃とは
日本語の情報としては Node.jsにおけるプロトタイプ汚染攻撃とは何か - ぼちぼち日記 が詳しいですが、まず、前提として、JavaScriptは「プロトタイプベースのオブジェクト指向」を採用しており、原則、すべてのプリミティブ型およびオブジェクトのインスタンスは「プロトタイプ」オブジェクトを参照しています1。
また、プロトタイプ・オブジェクト自身も別のプロトタイプ・オブジェクトを参照2しており、これにより、「クラスベース」のオブジェクト指向でいうところの継承関係を表しています。
プロトタイプのメンバーには、インスタンス・メソッド、クラス・メソッド、クラス・プロパティーに相当するものが存在します。
最終的にすべてのインスタンスは継承のrootである「Object」のプロトタイプを参照します。各「プロトタイプ」オブジェクトは、全く普通のオブジェクトであり、メンバーを自由に追加変更することができます。
プロトタイプのメンバーの変更は、それを参照するすべてのインスタンスが影響を受けます。この特徴を有益な形で使用すると、例えば、polifillとして古いブラウザーに存在しない機能を供給することができます。
一方で、よく呼ばれるであろう既存の任意のメソッド (例えば
toStringやvalueOf)を攻撃コードに入れ替えることができれば、これを起点に任意のコード実行が可能となります。({}).__proto__.toString = () => { alert('attack succeeded') }; ({}).toString(); // open the alert dialog.対策の結論
プロトタイプ汚染攻撃と任意の攻撃用関数の生成の組み合わせで大きな攻撃が可能となるので、
以下に示す対策でユーザーデータの実行(検証, パース, 式やコードのインタープリタ実行等)に対して汚染と機能の漏出を防がなければなりません。※ 従って、本対策内容はプロトタイプ汚染攻撃阻止に留まっていません。
※ プロトタイプ汚染攻撃のみでもリクエストの偽装やサーバーのクラッシュ等に利用できる可能性があり、また、未発見の他の脆弱性との組み合わせで大きな攻撃となるので放置してはいけません。
- 危険なプロパティー名へのアクセスは、読み取り・書き込みの両方を禁止する
- すべての
__proto__,__defineGetter__,__defineSetter__,__lookupGetter__,__lookupSetter__
- プロトタイプ汚染攻撃の本体に利用できる
- Functionコンストラクタ-のプロパティー(
Function.prototype)からもプロトタイプにアクセスできることに注意する
({}).toString.constructor.prototype等からもプロトタイプへのアクセスが可能なことに注意
'a'.toString.constructor.prototypeでも良い- 関数スコープの変数名
arguments,arguments.callee,arguments.caller
- プロトタイプ攻撃への自体への利用は難しいが、もし漏れるとFunctionコンストラクタ-へのアクセスに至るかもしれない
- グローバルオブジェクト自体および、その全プロパティーへのアクセスは、読み取り・書き込みの両方を禁止する
globalThis,window,global,this等から得ることができる
- メンバーアクセスではない関数呼び出しの
thisはglobalThisである- 下述の通り、Functionコンストラクターからも取得できる
evalはもちろんだが、その他ビルトインのオブジェクト、関数を置き換えられた時点で試合終了である- Objectコンストラクタ-自体(
Object)および、その全プロパティーへのアクセスは、読み取り・書き込みの両方を禁止する
- Objectコンストラクタ-かどうかは厳密比較演算子で判定すると良い
assignやdefinePropertyが呼び出し可能になると、プロトタイプ汚染攻撃の本体に利用できる- 非標準ではあるが
watch,unwatchも任意コード実行をトリガーできるため危険である- そもそも、メンバーを置き換えられたら終わりである
- Functionコンストラクター自体(
Function)および、その全プロパティーへのアクセスは、読み取り・書き込みの両方を禁止する
- Functionコンストラクターかどうかは厳密比較演算子で判定すると良い
- Functionコンストラクターからは、グローバルオブジェクトの取得が可能である
(Function('return this'))()
(({}).toString.constructor.call(null, 'return this'))()等とも記述できる- すべての関数からは
constructorプロパティーによってFunctionコンストラクターにアクセス可能なことに注意Function()とFunction.call()の両方が呼び出せれば、任意のコード実行が可能となるarguments,callerプロパティー経由でプロトタイプや意図しない情報にアクセスされる可能性がある- そもそも、関数型のオブジェクト
(typeof fun === 'function')の全プロパティーへのアクセスは、読み取り・書き込みの両方を禁止するべきである
- 上述で、Objectコンストラクター、Functionコンストラクターについて言及したが、関数オブジェクトは攻撃に有用なプロパティーを多く持っているので、アクセスさせてはならない
- また、上述2つ以外の有用なコンストラクターがあるかもしれないが、すべての関数はコンストラクターに成り得るため判定できないので、一括で禁止する
- オブジェクトは一般的な言語の「連想配列」ではない。そのまま使うのはプロトタイプ汚染が行われ危険である
Mapを使う- こちらが連想配列である
Object.create(null)でプロトタイプの無いオブジェクトを使うObject.freezeを使う- プロパティーのget/set時に都度チェックしてスキップする、またはエラーとする
Object.assign()によるケースもチェックする
Object.assign({}.__proto__, {toString: () => alert('attack succeeded')})で成功する
Object.assign({}, {__proto__: {toString: () => alert('attack succeeded')}})では成功しない詳細
上述で対策の結論は書いてしまいましたが、ここからは、それぞれの脆弱性ごとに例を交えて見ていきたいと思います。
? Critical
危険度 Critical な脆弱性です。
完全敗北です。自由に攻撃できます。1. 任意のテキストデータに対して
globalThis.eval関数3をコール可能にするこれは、プロトタイプ汚染攻撃ではありませんが、言うまでもなく即時アウトです。
eslintやtslintが入っていれば、呼び出すだけで警告となるでしょう。
10年前ならばあったかもしれないコードは以下のようなものではないでしょうか:// テキストで送られた通信のJSON形式ペイロードをオブジェクトに変換する var payload = eval(result.payload);JSONの標準サポートが無いブラウザーが生き残っていましたからね。
(注:当時からJSON変換用ライブラリーはありました。まともな人は使っていたはずです)もし、あなたが独自のインタープリタ作者であれば、JavaScriptの名前空間をインタープリタに公開するのは、その必要が本当にあるかどうか検討し、思い留まるべきです。
2. グローバルオブジェクト、オブジェクトコンストラクター、ファンクションコンストラクターを漏出する
グローバルオブジェクト(
globalThis,window,global)を漏らせば、そのメンバーからほぼすべてのビルトインオブジェクト・関数にアクセスできます。
evalもありますし、オブジェクトコンストラクター、ファンクションコンストラクターも得られます。
任意のコード実行が可能です。
ありとあらゆる機能が漏出したといって良いでしょう。オブジェクトコンストラクター(
Object)からはプロパティー操作に有用な関数にアクセスできますし、Object.prototypeからプロトタイプにアクセスできるので、プロトタイプ汚染攻撃が可能となります。ファンクションコンストラクター(
Function)からはcall関数によってグローバルオブジェクトが取得できるほか、任意の関数が呼び出せます。
また、ファンクションコンストラクター自身によって任意の関数を文字列から生成できます。
任意のコード実行が可能です。
こちらも、ありとあらゆる機能が漏出したといって良いでしょう。// ファンクションコンストラクターから任意の関数を作成し、グローバルオブジェクトを得る // その1 (Function('return this'))(); // その2 (({}).toString.constructor.call(null, 'return this'))();? High ~ Critical
危険度 High ~ Critical な脆弱性です。
単独の脆弱性で攻撃できます。
ただし、コード実行に至るかどうかはライブラリーやアプリの機能や使用方法に依存します。3. 利用頻度の高いビルトイン型のプロトタイプ・オブジェクトのメンバーに対し代入可能にする
プロトタイプ・オブジェクトは、インスタンスの
__proto__プロパティーからアクセスできます。
このように代入すると、派生先でtoStringをカスタマイズしていないオブジェクトのtoStringが呼ばれた時点で攻撃コードが実行されます。// 再帰的にオブジェクトをマージする function mergeObject(target, source) { for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (source[key] && typeof source[key] === 'object') { target[key] = mergeObject( target[key] && typeof target[key] === 'object' ? target[key] : {}, source[key]); } else { target[key] = source[key]; } } } return target; } const newPropsPicked = ...; // 攻撃者に細工されたオブジェクト // {__proto__: {toString: () => alert('attack succeeded')}} // value 部分に関数を注入できる機能または脆弱性があると、 // 攻撃コードが実行できる const currentProps = { /* 任意 */ }; mergeObject(currentProps, newPropsPicked);※
Objcet.assignが狙われることもあります。対策1
辞書を実装するのにオブジェクトを使うのを止めましょう。オブジェクトは一般的な言語の「連想配列」ではありません。
代わりにMapを使いましょう。const map = new Map(); map.set('__proto__', () => alert('attack succeeded')); // どんなキー名でも安全 const v = map.get('__proto__');課題:
- シリアライズが面倒
- 他のライブラリー/モジュールとのI/Fはオブジェクトであることが多い (変換が発生)
- 古いブラウザー対応が必要
- 機能の漏出は別途対策が必要
対策2
オブジェクトのプロトタイプをnullにします。
const currentScope = Object.create(null);課題:
- 他のライブラリー/モジュールにオブジェクトを受け渡すと、オブジェクトのメソッドを呼べないことを想定していないことが多い
- 機能の漏出は別途対策が必要
対策3
Object.freezeを使い、プロトタイプの変更を不能にしますObject.freeze(Object.prototype); Object.freeze(Object);課題:
- 他のライブラリー/モジュールが想定していないことがある
- 機能の漏出は別途対策が必要
対策4
危険なプロパティー名へのアクセス(読み取り・書き込み両方)をスキップするか、エラーとする
function checkDangerousThings(target, name) { ... if (name === '__proto__') { throw Error(`dangerous property ${name} is accessed`); } ... } // 再帰的にオブジェクトをマージする function mergeObject(target, source) { for (const key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { checkDangerousThings(target, key); if (source[key] && typeof source[key] === 'object') { target[key] = mergeObject( target[key] && typeof target[key] === 'object' ? target[key] : {}, source[key]); } else { target[key] = source[key]; } } } return target; } const newPropsPicked = ...; // 攻撃者に細工されたオブジェクト const currentProps = { /* 任意 */ }; mergeObject(currentProps, newPropsPicked);課題:
- 使用できないプロパティー名が多く生じる
- 格納する際のキー名をマングリングすれば、すべての名前を安全に使えるが、それをするくらいなら素直に
Mapを使ったほうがよい気がする(余談) Electronでのプロトタイプ汚染攻撃対策はもっと難しい?
Electron 6.xではオプションがかなり安全方向に振られるようになりましたが、安全ではないユーザーコンテンツを表示する際に、Node integrationや<webview>を有効化した場合、そのままメインプロセス側のAPIコールが可能となったり、remoteモジュール経由でメインプロセス側にプロトタイプ汚染攻撃が可能となったりします。公式ドキュメントをよく読み、オプションやハンドラーの追加で適切に機能を無効化しましょう。https://electronjs.org/docs/tutorial/security
参考資料
- 投稿日:2019-09-09T00:16:15+09:00
Node.jsでREST APIを連続実行するプログラムを組んでみる(Google Sheet APIを利用)
1.はじめに
業務で以下のような簡易プログラムが欲しくて作りたいので、下調べのためお試し版を作ってみたので記載する。
(1) REST API (HTTPメソッド:GET) を実行
(2) (1)で取得したデータを元にリクエストボディ作成
(3) REST API (HTTPメソッド:POST or PUT or DELETE) を実行使用するREST APIはなんでも良かったため、メジャーかつ使用するのに敷居が高くなさそうなのが良いと思い、Google Sheet APIを使用した。
実際は、REST APIを実行するために必要なアクセストークンの生成に必要な手順等で多少詰まったので、それもまとめておく。2. Google API OAuth2.0 のトークン取得
(1) Googleアカウント作成
アカウントがなければ。または、Google API用に別アカウントを作るのであれば
(2) Google Developers Consoleで新規プロジェクト作成
Google Developers Console にアクセス
画面上部「Google APIs」のロゴの隣にあるプロジェクトの選択の部分から新しいプロジェクトを作成
(3) Oauth Client ID 作成
認証情報からクライアントIDを作成する
作成したクライアントIDの右端のダウンロードボタンをクリックしてクライアントID等の情報が入ったJSONを取得する。
JSONの中身は以下のようになっている。{ "installed": { "client_id": "XXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com", "project_id": "dependable-aloe-XXXXX", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "XXXXXXXXXXXXXXXXXXXX", "redirect_uris": [ "urn:ietf:wg:oauth:2.0:oob", "http://localhost" ] } }(4) 認可画面を開いて認証
(3)で取得したJSONを元に以下URLのクエリパラメータを編集してアクセスする。
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id={client_id}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=https://www.googleapis.com/auth/spreadsheets%20https://www.googleapis.com/auth/drive&access_type=offline※scopeには使用したいAPIを指定する。以下のページからコピペ要
https://developers.google.com/identity/protocols/googlescopes※Google Sheet API を使用したい場合は、scopeにGoogle Drive APIも一緒に指定しないとAccessToken取得時にinvalid_grantエラーになるので注意
上記URLにアクセスして、scopeに指定したAPIを認証すると以下画面に「認証コード(Authorization Code)」が表示される
(5) Access Token 取得
(3)(4)で取得した情報を元に以下curlコマンドを組み立てて実行
※{authorization_code}は(4)の認証コード、{client_id},{clietn_secret}は(3)のJSONの値に置き換えcurl --data "code={authorization_code}" --data "client_id={client_id}" --data "client_secret={clietn_secret}" --data "redirect_uri=urn:ietf:wg:oauth:2.0:oob" --data "grant_type=authorization_code" --data "access_type=offline" https://www.googleapis.com/oauth2/v4/token(おまけ) Refresh Token で Access Token を再取得
(5)で取得した Access Tokenには有効期限があるため、一定時間経つと使用できなくなる。
以下curlコマンドでAccess Tokenの再取得が可能curl --data "refresh_token={refresh_token}" --data "client_id={client_id}" --data "client_secret={client_secret}" --data "grant_type=refresh_token" https://www.googleapis.com/oauth2/v4/token3. Node.jsによるRest API 連続実行
3.1. 下準備
(1) Node.js環境構築
$ npm init -y $ npm install request -- save $ npm install googleapis --save $ npm install fs --save(2) JavaScriptソースファイル(index.js等)を作成
(3) cledentials.json と token.json を用意
cledentials.jsonは2章(3)で取得したJSON
cledentials.json{ "installed": { "client_id": "XXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com", "project_id": "dependable-aloe-XXXXX", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "XXXXXXXXXXXXXXXXXXXX", "redirect_uris": [ "urn:ietf:wg:oauth:2.0:oob", "http://localhost" ] } }token.jsonは2章(5)のレスポンス
token.json{ "access_token":"XXXXXXXXXXXXXXXXX", "expires_in":3600, "refresh_token":"XXXXXXXXXXXXXXXXX", "scope":"https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive", "token_type":"Bearer" }(4) 以下公式のサンプルコードを改造して作成するので理解する
https://developers.google.com/sheets/api/quickstart/nodejs
※上記サンプルコードで、2章(4),(5)は半自動で実施可能。実行にはcledentials.jsonが必須。
3.2. 実際に作ったプログラム
(題材はなんでも良かったが)ポケモンの種族値から実際のステータス値を算出/出力するプログラムを作ってみた(真面目にやると複数の変動値を考慮する必要があるのでその辺は固定値にした)。
1回の実行で、ソース内で指定しているGoogleスプレッドシートに対して、REST API(Google Sheet API)を連続で実行して以下を行う。
(1) BaseStatsシートのB3~G5の範囲のデータ(種族値)取得
ソース上の対応メソッド:getSpreadSheetData()
(2) StatisticsシートのB3~G5の範囲にデータ(計算結果の実数値)書き込み
ソース上の対応メソッド:writeSpreadSheet()
↓
(3) REST API実行時にAccess Tokenの有効期限切れのエラーとなった場合はAccess Tokenを再取得して1回だけ再実行する。
ソース上の対応メソッド:retryCall()
・ソースコード
※authorize(), getNewToken()は参考サイト( https://developers.google.com/sheets/api/quickstart/nodejs )とほぼ同じ
※google.sheetsを使用すればわざわざURL指定をする必要ないが、今回の趣旨に反するためあえて使用していない。
※readFile()やrequest.get(), request.post()等は非同期実行なので注意するindex.jsconst request = require('request'); const { google } = require('googleapis'); const fs = require('fs'); // constant const CLEDENT_PATH = 'credentials.json'; const TOKEN_PATH = 'token.json'; const READ_SHEET_URL = 'https://sheets.googleapis.com/v4/spreadsheets/1zi8P6wQujXtLvPriViF9Z82-6VlDrBUvUdb_dlgSwnA/values/BaseStats!B3:G5'; const WRITE_SHEET_URL = 'https://sheets.googleapis.com/v4/spreadsheets/1zi8P6wQujXtLvPriViF9Z82-6VlDrBUvUdb_dlgSwnA/values/Statistics!B3:G5'; const REFRESH_URL = 'https://www.googleapis.com/oauth2/v4/token'; // get Access Token fs.readFile(CLEDENT_PATH, (err, content) => { if (err) return console.log('Error loading client secret file:', err); // Authorize a client with credentials, then call the Google Sheets API. authorize(JSON.parse(content), getSpreadSheetData); }); function authorize(credentials, callback) { const { client_secret, client_id, redirect_uris } = credentials.installed; const oAuth2Client = new google.auth.OAuth2( client_id, client_secret, redirect_uris[0]); // Check if we have previously stored a token. fs.readFile(TOKEN_PATH, (err, token) => { if (err) return getNewToken(oAuth2Client, callback); oAuth2Client.setCredentials(JSON.parse(token)); callback(oAuth2Client, false); }); } function getNewToken(oAuth2Client, callback) { const authUrl = oAuth2Client.generateAuthUrl({ access_type: 'offline', scope: SCOPES, }); console.log('Authorize this app by visiting this url:', authUrl); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); rl.question('Enter the code from that page here: ', (code) => { rl.close(); oAuth2Client.getToken(code, (err, token) => { if (err) return console.error('Error while trying to retrieve access token', err); oAuth2Client.setCredentials(token); // Store the token to disk for later program executions fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => { if (err) return console.error(err); console.log('Token stored to', TOKEN_PATH); }); callback(oAuth2Client, false); }); }); } // read Spread Sheet function getSpreadSheetData(outh2Client, isRetry) { request.get({ uri: READ_SHEET_URL, headers: { 'Content-type': 'application/json' }, qs: { access_token: outh2Client.credentials.access_token, }, json: true }, function (err, req, data) { if (data.error) { retryCall(data.error, outh2Client, getSpreadSheetData, isRetry) } else { console.log("--- Result Sheet Reading ---\n" + JSON.stringify(data)); let writeValues = generateStats(data.values); writeSpreadSheet(outh2Client, writeValues, false); } }); } // write Spread Sheet function writeSpreadSheet(outh2Client, writeValues, isRetry) { request.put({ uri: WRITE_SHEET_URL, headers: { 'Content-type': 'application/json' }, qs: { access_token: outh2Client.credentials.access_token, valueInputOption: "USER_ENTERED" }, json: { "range": "Statistics!B3:G5", "majorDimension": "ROWS", "values": writeValues } }, function (err, req, data) { if (data.error) { retryCall(data.error, outh2Client, writeSpreadSheet, isRetry) } else { console.log("--- Result Sheet Writing ---\n" + JSON.stringify(data)); console.log("writeData: " + JSON.stringify(writeValues)); } }); } // retry rest api execute function retryCall(error, outh2Client, callback, isRetry) { if (!isRetry && error.code === 401 && error.status === "UNAUTHENTICATED") { refreshToken(outh2Client, callback); } else { console.log("[api error]"); console.log(data); return; } } // refresh Access Token function refreshToken(outh2Client, callback) { request.post(REFRESH_URL, { form: { grant_type: 'refresh_token', refresh_token: outh2Client.credentials.refresh_token, client_id: outh2Client._clientId, client_secret: outh2Client._clientSecret } }, function (err, res, data) { data = JSON.parse(data); if (data.error) { console.log("[error refresh failure]"); console.log(data); } else { outh2Client.credentials.access_token = data.access_token; fs.writeFile(TOKEN_PATH, JSON.stringify(outh2Client.credentials), (err) => { if (err) return console.error(err); console.log('Token stored to', TOKEN_PATH); }); callback(outh2Client, true); } }); } // calculate pokemon Statistics function generateStats(baseStatsList) { let statsList = []; for (baseStats of baseStatsList) { let stats = []; stats.push(calculateHP(baseStats[0])); for (let i = 1; i < baseStats.length; i++) { stats.push(calculateOther(baseStats[i])); } statsList.push(stats); } return statsList; } const level = 50; const individualValue = 31; const effortValue = 0; const natureCorrectionRate = 1; function calculateHP(baseStat) { let stat = parseInt((baseStat * 2 + individualValue + effortValue / 4) * (level / 100)) + 10 + level; return stat; } function calculateOther(baseStat) { let stat = parseInt((parseInt((baseStat * 2 + individualValue + effortValue / 4) * (level / 100)) + 5) * natureCorrectionRate); return stat; }・実行後のターミナル出力結果(例)
PS C:\developments\vsCode\restNodeJs> node index.js --- Result Sheet Reading --- {"range":"BaseStats!B3:G5","majorDimension":"ROWS","values":[["80","82","83","100","100","80"],["78","84","78","109","85","100"],["79","83","100","85","105","78"]]} --- Result Sheet Writing --- {"spreadsheetId":"1zi8P6wQujXtLvPriViF9Z82-6VlDrBUvUdb_dlgSwnA","updatedRange":"Statistics!B3:G5","updatedRows":3,"updatedColumns":6,"updatedCells":18} writeData: [[155,102,103,120,120,100],[153,104,98,129,105,120],[154,103,120,105,125,98]]参考サイト
・Google APIのAccess Tokenをお手軽に取得する
https://qiita.com/shin1ogawa/items/49a076f62e5f17f18fe5・Google APIを使用するためにGoogle OAuth認証をしようよ
https://himakan.net/websites/how_to_google_oauth・【Google API入門(1)】Google OAuthでAccess Tokenを取得してみる
https://poppingcarp.com/google-api_get_access_token/・Node.jsでGoogle APIをOAuth2.0認証してAPIを使う方法
https://photo-tea.com/p/17/nodejs-google-api-oauth/・Node.jsからWebAPIを叩く
https://qiita.com/yuta0801/items/ff7f314f45c4f8dc8a48・node.js – Google OAuthがリフレッシュトークンを使用して新しいアクセストークンを取得する
https://codeday.me/jp/qa/20190625/1095976.html・Google Sheet API v4 ガイド
https://developers.google.com/sheets/api/quickstart/nodejs・Google Sheet API v4 サンプル
https://developers.google.com/sheets/api/samples/













































