20210730のJavaScriptに関する記事は13件です。

拡張機能でContent-Security-Policyを回避したい (VivaldiでGoogle翻訳を使いたい)

動機 vivaldi搭載の翻訳では精度が良くないのでGoogle翻訳を使いたい ページにGoogle翻訳スクリプトを埋め込む方式の拡張機能だとGithubなどのページではContent-Security-Policy(CSP)が設定されておりそのスクリプトが実行できない CSPはHTTPヘッダで制御されているのでヘッダを書き換えればCSPは無効化できる ヘッダの書き換えは拡張機能で行える 方法 今回、ヘッダの書き換えは既存の翻訳拡張機能を改造することで行う 改造したい拡張機能のフォルダを「パッケージ化されていない拡張機能を読み込む」で読み込む 下記のように manifest.json の background に scripts として background.js を指定する permissions に webRequest , webRequestBlocking を追加する manifest.json { // ... "background": { "scripts": ["background.js"] }, // ... "permissions": [ "tabs", "http://*/", "https://*/", "contextMenus", "webRequest", "webRequestBlocking" ], // ... } background.js にヘッダを削除する js を追記する background.js // ... chrome.webRequest.onHeadersReceived.addListener(function(detail){ const headers = detail.responseHeaders.filter(e => e.name !== "content-security-policy") return {responseHeaders: headers} }, {urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]) このjsでヘッダ内のcontent-security-policyを消して読み込むことができる 2つ目の引数のオブジェクトのプロパティ urls には <all_urls> で全てのページが対象になる これを行うことでXSSなどの攻撃に対するセキュリティレベルが低下するので注意(urlsで都度指定するのが良い)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Vue.js+Vuetify]段階評価ボタン ~評価項目の数と評価対象の数に応じて動的に表示する~

概要 v-btn-toggleで生成した画像のような段階評価ボタンをコンポーネント化し、評価項目の数だけイテレートする。 今回の使用例 生徒一人ひとりの各教科の成績を7段階評価するシステム。 生徒と科目のデータは外部入力などで取得する動的な値。 イメージ コンポーネント構成 孫: EvaluationItem.vue 一組のトグルボタン 科目の数だけ表示する。 子: EvaluationList.vue 複数のトグルボタンのリスト 生徒の数だけ表示する。 親: index.vue  孫コンポーネント(トグルボタンコンポーネント) EvaluationItem.vue <template> <v-btn-toggle v-model="value" @change="sendValue" > <v-btn v-for="n in 7" :key="n" :value="n" > {{ n }} </v-btn> </v-btn-toggle> </template> <script> export default { name: 'EvaluationItem', data() { return { value: null } }, methods: { sendValue() { this.$emit('catch-value', this.value) } } } </script> 1~7の数字が書かれたボタン。この数字は評点(value)とバインドしている。 ボタンが押される(changeイベント)のたびにvalueがリストコンポーネントに送られる 子コンポーネント(リストコンポーネント) EvaluationList.vue <template> <div v-for="(subject, index) in subjects" :key="index" > {{ subject }} <EvaluationItem @catch-value="sendData(index, $event)" /> </div> </template> <script> import EvaluationItem from './EvaluationItem.vue' export default { name: 'EvaluationList', components: { EvaluationItem }, props: { subjects: { type: String, required: true } }, data() { return { evalListData: [] } }, methods: { // リスト内の全ての科目の評点がつけられたかを検証するメソッド isInputDataEnough(arr) { const l = arr.filter(v => v).length // filterによって空要素が除去される return l == this.subjects.length ? true : false // 空要素を除去した後の要素数が科目数と等しければ全て評点がつけられたということ }, sendData(index, value) { // 送られてきた評点を表示順に配列に格納し、全部揃い次第親クラスに配列を送る this.evalListData[index] = value if (this.isInputDataEnough(this.evalListData)) { this.$emit('catch-data', this.evalListData) } } } </script> リスト内で科目の数だけトグルボタンコンポーネントがイテレートされており、それぞれの上に科目名subjectが表示されている。 科目の配列subjectsは親コンポーネントから渡されている。 トグルボタンコンポーネントでchangeイベントのたびに評点が送られてくるとともに、sendDataメソッドが発火する。 それぞれのトグルボタンコンポーネントのリスト内における表示順indexと評点valueが渡されており、評点はその科目の表示順通りに配列evalListDataに格納される。 全ての科目の評点がつけられたとき、評点を格納したevalListDataが親コンポーネントに送られる。 親コンポーネント(ページコンポーネント) index.vue <template> <v-container> <div v-for="(student, index) in students" :key="index" > {{ student }} <EvaluationList :subjects="subjects" @catch-data="setEvalDataCollection(index, $event)" /> </div> </v-container> </template> <script> import EvaluationList from './components/EvaluationList.vue' export default { components: { EvaluationList, }, data() { return { evalDataCollection: [], } }, computed: { subjects() { return // 科目(動的な値) }, students() { return // 生徒(動的な値) } }, methods: { setEvalDataCollection(index, array) { // 複数のリストコンポーネントから送られてきた評点配列たちを表示順通りに格納 this.evalDataCollection[index] = array } } } それぞれのリストで全ての科目の評点が終わり次第、リストコンポーネントから評点が格納された配列が送られてきて、setEvalDataCollectionメソッドが発火する。 それぞれのリストコンポーネントのページ内における表示順indexと評点配列arrayが渡されており、arrayは表示順通りに配列evalDataCollection内に格納される。 students = ['佐藤', '井口', '新井'] subjects = ['国語', '算数', '理科'] のとき evalDataCollection = [ [佐藤の国語の評点, 佐藤の算数の評点, 佐藤の理科の評点], [井口の国語の評点, 井口の算数の評点, 井口の理科の評点], [新井の国語の評点, 新井の算数の評点, 新井の理科の評点] ]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Electronでウインドウアプリ製作 part1

目標 electronを用いてフレームレス(タイトルバーのない)なウインドウアプリケーションを作成し、その際独自に最小化、最大化、閉じるボタンを作り、機能させる。 下準備 Node.jsのインストール 割愛します、詳しくはこちら アプリケーションのディレクトリを作成 ディレクトリ名は仮にTestWindowAppとした。 $ mkdir TestWindowApp $ cd TestWindowApp package.jsonの作成 Node.jsのパッケージを管理するnpm(Node Package Manager)を使い、packege.jsonを生成する。 $ npm init -y 生成されたpackage.json package.json { "name": "testwindowapp", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" } Electronのインストール $ npm i -D electron カレントディレクトリにElectronがインストールされる。 これにより、node_modulesフォルダと、package-lock.jsonが生成されます。 ディレクトリ構成 TestWindowApp ├ node_modules/ ├ package-lock.json └ package.json 各種ディレクトリ、ファイルの作成 カレントディレクトリにsrcフォルダを作成。 更にその中に、index.html、main.js、package.json を作成します。 index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Test Window App</title> </head> <body> <!-- header area --> <div class="header-area"></div> <!-- main area --> <div class="main-area">Hello World!</div> <!-- footer area --> <div class="footer-area"></div> </body> </html> main.js const { app, Menu, BrowserWindow, ipcMain } = require('electron'); const path = require('path'); const url = require('url'); let mainWindow; function createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600, }); mainWindow.loadURL(url.format({ pathname: path.join(__dirname, '/index.html'), protocol: 'file:', slashes: true })); // コメントアウトで開発者ツール起動 // mainWindow.webContents.openDevTools(); mainWindow.on('closed', () => { mainWindow = null; }); } app.on('ready', createWindow); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createWindow(); } }); package.json { "main": "main.js" } これらを作成したら、下記コマンドで実行するとブラウザが立ち上がります。 実行 $ npx electron src このようなウインドウが表示された。 が、タイトルバーやツールバーがちょっと邪魔だなと思った。 そういう時は main.js mainWindow = new BrowserWindow({ width: 800, height: 600, frame: false }); とすると、これらが非表示となりフレームレスなウインドウが表示される。 Visual Studio Codeで実行する Electronでアプリを作っている時に、いちいち npx electron src をするのはめんどくさい。 ということで、起動構成を追加する。 main.jsを選択した状態で、VSCode上の構成の追加を行う際、Node.jsを選択できます。 Node.jsを選択して起動構成を追加すると、 ルートディレクトリの直下に.vscodeファイルが、更にその中にlaunch.jsonが生成されます。 launch.json { // IntelliSense を使用して利用可能な属性を学べます。 // 既存の属性の説明をホバーして表示します。 // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "pwa-node", "request": "launch", "name": "Launch Program", "skipFiles": [ "<node_internals>/**" ], "program": "${workspaceFolder}\\src\\main.js" } ] } このままでは動かないので、launch.jsonを以下のように変更した。 launch.json { "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Electron Main", "program": "${workspaceFolder}/src/main.js", "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", "runtimeArgs": [ "--enable-logging" ], "console":"integratedTerminal" } ] } これでVisual Studio Code上で実行して、Electronブラウザを立ち上げることが出来るようになりました。 簡易的なレイアウトの作成 srcフォルダ直下にcss, imgフォルダを追加しました。 cssフォルダ内に、2つのcssファイルを作成 imgフォルダ内に、最小化、最大化、閉じるボタンの画像を用意 index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Test Window App</title> <link rel="stylesheet" href="css/main.css"> <link rel="stylesheet" href="css/parts.css"> </head> <body> <!-- header area --> <div class="header-area"> <div class="header-container"> <div class="button-tools"></div> <div class="title-bar">Test Window App</div> <div class="button-control"> <!--最小化、最大化、閉じるボタンが格納されるエリア--> <div class="button" id="button-minimize"> <button><img src="img/minimize.png"></button> </div> <div class="button" id="button-maximize"> <button><img src="img/maximize.png"></button> </div> <div class="button" id="button-close"> <button><img src="img/close.png"></button> </div> </div> </div> </div> <!-- main area --> <div class="main-area"> <div class="main-container"> <p>Hello World!</p> </div> </div> <!-- footer area --> <div class="footer-area"> <div class="footer-container"></div> </div> </body> </html> メインウインドウのレイアウト担当 main.css * { margin: 0px; padding: 0px; } html, body { width: 100%; height: 100%; overflow: hidden; background-color: #e4edf1; } /* ヘッダーエリア */ .header-area { position: fixed; -webkit-app-region: drag; /* draggable */ height: 34px; width: 100%; } .header-container{ display: flex; height: 100%; } .button-tools{ width: 10%; } .title-bar{ display: inline-flex; justify-content: center; align-items: center; flex: 1; font-size: 14px; } .button-control{ display: flex; width: 160px; } /* メインエリア */ .main-area { /* ヘッダーが34px、フッターが20pxのため余白に */ padding: 34px 0px 20px 0px; height: 546px; } .main-container { width: 100%; height: 100%; background-color: #ffffff; } /* フッターエリア */ .footer-area { position: fixed; height: 20px; width: 100%; bottom: 0px; } ボタン等の細かなパーツの装飾担当 parts /* ボタン装飾 */ button{ width: 100%; height: 100%; border: none; outline: none !important; background: transparent; } .button{ display: inline-flex; justify-content: center; align-items: center; text-align: center; height: 100%; width: 100%; -webkit-app-region: no-drag; } #button-minimize{ flex: 1; } #button-maximize{ flex: 1; } #button-close{ flex: 1; } #button-minimize:hover{ background-color: #aed5fa; } #button-maximize:hover{ background-color: #aed5fa; } #button-close:hover{ background-color: #ff4747; } この状態で実行すると、画像のようになります。 次回 今回追加したボタンにクリックイベントを割り当てます。 (未完成)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Sequelize】configファイルで環境変数を使いたい

sequelize-cli npm install --save-dev sequelize-cli npx sequelize-cli init こうするとRubyのActiveRecord風にSequelize経由でdbを操作することができるようになる。また、initで生成されたconfig/config.jsonからdbへの接続情報を指定することになる。 config.json { "development": { "username": "root", "password": null, "database": "database_development", "host": "127.0.0.1", "dialect": "postgres" }, "test": { "username": "root", "password": null, "database": "database_test", "host": "127.0.0.1", "dialect": "postgres" }, "production": { "username": "root", "password": null, "database": "database_production", "host": "127.0.0.1", "dialect": "postgres" } } でも、jsonにパスワードなどを直接記入するのは避け、.envを使いたいところ。 .sequelizerc そこで.sequelizercの登場。 プロジェクトのルートで touch .sequelizerc .sequelizerc const path = require('path'); module.exports = { 'config': path.resolve('./config', 'config.js'), }; そして、先程のconfig.jsonをconfig.jsにリネームし、内容を下記に書き換える。 config.js module.exports = { development: { username: process.env.POSTGRES_USER, password: process.env.POSTGRES_PASSWORD, database: 'development', host: '127.0.0.1', dialect: 'postgres', }, } このようにすることで、sequelizeのconfigに環境変数が利用できるようになった。 参考 Sequlize公式ドキュメント
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】要素の取得したときの返り値の違い(HTMLCollection, NodeList)

はじめに JavaScriptでHTMLの要素を取得する方法はいくつかあります。 jQueryでは$('セレクタ')と書いて何も考えずに取得していたのに、生のJavaScriptでは少し気を使わないといけません! 今回は取得方法による違いを見ていき、取得方法の使い分けを簡単にまとめてみたいと思います。 そもそもなんでHTML要素を取得したいの? JavaScriptを使用する理由の目的を簡潔にいうとHTML(DOM)操作がしたいからではないでしょうか? そのためどの要素に対して(または基準として)操作をするか指定するために要素取得を行います。 JavaScript // classを追加したり document.getElementById('hoge').classList.add('moge'); // 子要素を追加したり const p = document.createElement("p"); p.textContent = 'こんにちは!'; document.querySelectorAll('.moge')[0].appendChild(p); 簡単な例ですが、 このようにJavaScriptの大通りにはHTML要素取得が必要となってきます。 要素取得してみる HTML要素取得の必要性がなんとなくわかったところで、HTML要素を取得する方法を何個か見ていきましょう! こういうHTMLがあったとして HTML <ul id="list"> <li class="item">アイテム</li> <li class="item">アイテム</li> <li class="item">アイテム</li> <li class="item">アイテム</li> </ul> JavaScriptでよく見るHTML取得方法を試してみる JavaScript document.getElementsByClassName('item'); // => HTMLCollection(4) document.querySelectorAll('.item'); // => NodeList(4) document.getElementById('list'); // => <ul id="list">...</ul> 取得は出来ているようですが、それぞれ返り値が違うようです(混乱) 返り値の違い(HTMLCollection, NodeList) さて本題です。それぞれの返り値を見ていきましょう! HTMLCollection Element(後述)が格納されている配列風オブジェクト(Array-Like Object) 取得した要素数を格納しているlengthプロパティを持っている オブジェクトに[index]または.item(index)メソッドを使用してElementを取り出せる 取得する要素が一個しかない場合でも配列風オブジェクトで返ってくるので[0]で取り出す 要素がなければlengthが0の空の配列風オブジェクトが返ってくる あくまで配列風なのでforEachは使えない es6対応ブラウザならArray.from(配列風オブジェクト)で変換 es6未対応なら[].slice.call(配列風オブジェクト)で変換 単純なforなら可能 取得したElementたちを動的に保持している DOMの要素数の影響を受ける 要素が4つの状態で取得後、同じ要素がDOM上で5つに増えたら取得したオブジェクトの中身も5つになっている 全体的にNodeListと似ている(詳しく調べられていませんが歴史的背景がありそう) NodeList Element(後述)が格納されている配列風オブジェクト(Array-Like Object) 取得した要素数を格納しているlengthプロパティを持っている オブジェクトに[index]または.item(indesx)メソッドを使用してElementを取り出せる 取得する要素が一個しかない場合でも配列風オブジェクトで返ってくるので[0]で取り出す 要素がなければlengthが0の空の配列風オブジェクトが返ってくる 単純なforなら可能 配列風オブジェクトだがforEachは使用できる IE11はバグでNodeListにforEachは使えないので、[].slice.call(配列風オブジェクト)で変換 取得したElementたちを静的に保持している DOMの要素数の影響を受けない 要素が4つの状態で取得後、同じ要素がDOM上で5つに増えても取得したオブジェクトの中身は4つのまま 全体的にHTMLCollectionと似ている(詳しく調べられていませんが歴史的背景がありそう) Element 要素のこと(呼び方が若干違うかもれないですがこの記事ではElementと呼ぶ) 上の例で言うとgetElementById('list')で取得した<ul id="list">...</ul> HTMLで書かれたものが返ってくるので馴染みやすい 要素がなければnull 配列風オブジェクトでforEachなど使わずにHTML要素を操作したい場合は最終的にElementを指定する 例) 配列風オブジェクト[0]、配列風オブジェクト.item(4) 結局どれの取得方法を使えばいい? 全体像 NodeList querySelectorAll HTMLCollection getElements* (getElementsByClassNameとかgetElementsByTagNameとか複数とることが前提のやつ) Element getElementById querySelector (一番はじめにヒットするものが取得できる) 配列風オブジェクトをインデックス指定した時 (配列風オブジェクト[0]など) 使い分け ◇HTML上に一つしかない要素を取得したい Elementで取れればいいのでquerySelector →CSSやjQueryみたいにセレクタで指定できるからいいよね ◇複数要素を取得したい 扱いやすい配列で取れればいいのでquerySelectorAll →CSSやjQueryみたいにセレクタで指定できるからいいよね →NodeListなのでIE11でforEach使う場合は注意 →取得した値はDOMの変化に同期されないので注意。 ◇要素が増減するので動的に取得したい HTMLCollectionで取れる`getElements*
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【vue.js】npm run devでストア(store.js)を動かせるようになるまで

store.jsがどうしても動かなかった Vue.jsの初学者として猫本を進めることしばらく、Vuexを導入しp255以降のstore.jsを実装する段に到達。まずはシンプルなものをとS42 シンプルなストア構造に挑戦しました。 が、動かない。コマンドラインでいくらnpm run devしてもエラーを吐くばかり。 最終的には解決して結構しょうもないオチだったものの、苦しめられたので記録しておきます。同様の事例はあれど解決ログはなさそうだったので。 インストールまで まず猫本p254までの記載通り、npmで一通りvuex関連のinstallを済ませます。すると「my-app」ディレクトリが作成されます。今回変更が必要なのは、その中の「src」ディレクトリだけです。 src直下にあるmain.jsを変えないまま、my-appでコマンドプロンプトを起動しnpm run devを打つと、http://localhost:8080/#/でwebサーバーが立ち上がります。V字マークのページが表示されれば成功です。 その時点のmain.jsのソース。 // The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '<App/>' }) store.jsの作成とmain.jsの修正 そしてp255の『シンプルなストア構造』へ。 今回あまりにも動かなかったので、猫本サポートページやVuex公式ドキュメント、その他様々な記事を見ながら格闘していました。 そして出来上がった動かない版のmain.jsとstore.jsがこちら。 main.js // The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' //import store from './store.js' // ここを追記 Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', store, // ここも追記 router, components: { App }, template: '<App/>' }) import store from '@/store.js' console.log(store.state.count) // -> 0 // incrementをコミットする store.commit('increment') // もう一度アクセスしてみるとカウントが増えている console.log(store.state.count) // -> 1 store.js import 'babel-polyfill' import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // ストアを作成 const store = new Vuex.Store({ state: { count: 0 }, mutations: { // カウントアップするミューテーションを登録 increment(state) { state.count++ } } }) export default store エラーメッセージ ERROR Failed to compile with 1 errors 13:31:58 error in ./src/main.js Module build failed: Error: ENOENT: no such file or directory, open 'C:\Users\suer\Pictures\ITエンジニアスキル\Vue.js\ コード\my-app\src\main.js' @ multi (webpack)-dev-server/client?http://localhost:8080 webpack/hot/dev-server ./src/main.js WAIT Compiling... 13:32:05 10% build 30 18 65% buil 95% emitting WARNING Compiled with 2 warnings 13:32:06 ✘ http://eslint.org/docs/rules/space-before-function-paren Missing space before function parentheses src\store.js:13:14 increment(state) { ^ ✘ http://eslint.org/docs/rules/eol-last Newline required at end of file but not found src\store.js:18:21 export default store ^ ✘ 2 problems (2 errors, 0 warnings) Errors: 1 http://eslint.org/docs/rules/space-before-function-paren 1 http://eslint.org/docs/rules/eol-last ✘ http://eslint.org/docs/rules/spaced-comment Expected space or tab after '//' in comment src\main.js:6:1 //import store from './store.js' // ここを追記 ^ ✘ http://eslint.org/docs/rules/indent Expected indentation of 2 spaces but found 0 src\main.js:13:1 // store, // ここも追記 ^ ✘ https://google.com/#q=import%2Ffirst Import in body of module; reorder to top src\main.js:19:1 import store from '@/store.js' ^ ✘ 3 problems (3 errors, 0 warnings) Errors: 1 http://eslint.org/docs/rules/spaced-comment 1 http://eslint.org/docs/rules/indent 1 https://google.com/#q=import%2Ffirst You may use special comments to disable some warnings. Use // eslint-disable-next-line to ignore the next line. Use /* eslint-disable */ to ignore all warnings in a file. エラー要因と解決 結論から言いますと、空白行の不足や空白の入れ方によるものでした。 なんでも検索しているうちにESLintというのが引っ掛かりまして、これは静的検証ツールだそうです。確かにパッケージインストールの際にそんなものを入れたような気もします。 これがスタイルの統一を求めるため、空白の入れ方をエラーとして検知していたようです。なので以下の動作版も、見た目は特にエラー版と変わりありません。 なおコピペした場合最終行の空白行が消えてしまったりしてエラーになることがあります。猫本や公式ページから移してきても動かない、おかしいな……というのはその辺りが原因でした。また、// でのコメントアウトも何故か弾かれるようで、/* */と書く必要があります。 main.js // The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' import store from './store.js' Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', store, router, components: { App }, template: '<App/>' }) store.commit('increment') console.log(store.state.count) store.js import 'babel-polyfill' import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // ストアを作成 const store = new Vuex.Store({ state: { count: 0 }, mutations: { // カウントアップするミューテーションを登録 increment (state) { state.count++ } } }) export default store 半角スペースを二回入れている箇所にも指摘が入ったりしていたので、エラーログの言う通りに見ていくと動くようになりました。 本体はなんとなくわかってきたけどライブラリわからん……と唸る前にエラーログはしっかり読もう、という学びですね。 (初記事ですが、ここまで読んでくれた方ありがとうございます。解決の役に立ちましたら幸いです)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

安定稼働(放置)していたGASのWebアプリがある日動かないと言われた(長編ポエム)

GASで作ったWebアプリがあって、安定稼働していたので(特に文句が出なかったので)油断して放置していたのだが、ある日急にバグってると言われた。 Chromeの仕様変更によるものだが、半年も放置していたので、副次的に色んなトラブルが起こってハマった。 徒然なるままに書くので暇な人だけ読んでね。 概要 以下の現象が起こっていた。 Chromeの最新版でCORSが禁じられたのでGASではないJavaScriptのalert()とwindow.confirm()が動かなくなった。それを出しているSUBMITボタンが効かなくなった Chromeにオプションをつけて制限をゆるめれば動くという話もあったけど、今回はダメだった。 単純にalert()とwindow.confirm()関数を削除するとSUBMITが効くようになった 話は変わるが、しばらく使っていないGASのWebアプリを使ったり、しばらくそのアプリを使っていないアカウントから使おうとすると「その操作を実行するには承認が必要です」と言われる。スクリプトエディターからデプロイをやり直すと動く 削除したalert()とwindow.confirm()の代替案はまだやってない。JSONPというのを使うらしい? そのうちやるかも ある日動かなくなった GAS+clasp+TypeScript+WSLでWebアプリを書いている。 ファイルをアップロードしたりGスプレッドシートを書き換えたりするというもの。 難しいことはしてません。 開発が終わって半年余。 特に文句は出なかったので油断しきっていた。 開発環境もテストアカウントも触っていなかったのである。 そんなある日、動かなくなったとユーザーから言われた。 SUBMITボタンをクリックすると、ファビコンがクルクル回って更新が起こるはずだが、起きないという話。 自分の環境では通常に動作していたので「Chromeのキャッシュを削除したり、Windowsを再起動したり、Chromeを最新版にしたり、キャッシュをクリアしたりしてみてください」と言ったが、「そのへんのことは全部やりました」そりゃそうだね。 放っておいたら同様のレポートが続々と届き、対策することになった。 iPhoneでやったら動く なぜかiPhone版のChromeだと動くので、それでしのいでもらった。 でも承認ボタンを押すぐらいならそれで済むけど、メッセージを書いたりファイルをアップロードしたりさせるのは気が重い。 対策に乗り出した。 開発版アプリが動かない いちおうアプリを開発版と本番用で分けているので、開発版で駆動してみた。 だが、ボタンが押せないどころか、「その操作を実行するには承認が必要です」と言われて真っ白な画面になる。 このメッセージでググると、たいてい下の対策を勧められるが、解決しなかった。 トリガーを追加したり、削除して再作成したりしてください<=そもそもなかった Developer Hubから、Google Apps Scriptをオンにしてください<=もともとオンだった 万事休す。 アカウントの問題だろうと思って、アカウント=>セキュリティ=>サードパーティアプリというところで? くだんのアプリを見つけた。 本番用はあるけど、開発版がない。 なんとなく本番用を削除してみたら、さっきまで使えていた本番用も同じメッセージが出て使えなくなった。 ヒエー? 開発環境がリニューアルされている ようやく本腰を入れて対応する気になって、スクリプトエディターを開いてみた。 なんか様子が違う。 この半年の間に、Googleによってリニューアルされているのだ。 そのうち使ってみたいとは思うけど、こういう切羽詰まった時に状況を複雑にするものではないと思ってレガシー版に戻した。 「リニューアル版の何がお気に召さなかったでしょうか」みたいなアンケート画面が出てくる。 うっせーわ。 とりあえずクローズさせてもらう。 レガシー版に戻しても、「新しいエディタを使用」ボタンが表示されてる。 ま、そのうちな。 デプロイ(公開)をやり直した 何がなんだか分からないのだが、とりあえずコードは1文字も変えずにデプロイだけやり直してみた。 デプロイ、それは開発したアプリを配布することで、スクリプトエディターからは公開=>ウェブアプリケーションとして導入で行う。 これをやってみたのだ。 (バージョン番号が1つ上がる) なぜ上記の行為に出たか、自分でも分からないのだが、なんとなく、それだけのことで、「その操作を実行するには承認が必要です」の問題が解決した。 あと、開発用アカウントからアクセスしても、同じメッセージが出て困っていたが、さらにもう1回デプロイしてみたらなんとなく治った。 こういう感じのようだ。 しばらく使っていないウェブアプリケーションを起動すると「その操作を実行するには承認が必要です」と言われることがある。デプロイをやり直すと治る しばらく使っていないアカウントからウェブアプリケーションを起動すると「その操作を実行するには承認が必要です」と言われることがある。デプロイをやり直すと治る フワッフワした話ですみませんが、どうもこういうことのようだ。 新しいChromeからだとダメ それにしてもなぜ自分では再現できないのだろうか。 開発者は、孫を抱くように優しく自分のアプリを扱うし、ついつい危ない操作は避けるので、開発者がテストするとバグが出せないのはあるあるだけど、やっかいな現象だ。 ふと気づくと、Chromeの右上に「更新」というボタンが表示されている。 なんと驚きなされ。 ユーザーのみなさんには「最新版のChromeを使っていますか」と言っていた開発者が、なんと自分は古いバージョンのChromeを使っていたのである!! これは問題やろう。 具体的に言うと、91.0.4472.124では動作していたアプリが、92.0.4515.107では動作しなくなった。 ひどいな俺。 iPhoneのChromeはバージョンが古いんだろう。(※2021-07-30追記:本件コメントいただきました) 問題はCORS ということで、晴れて問題が出るようになった。 具体的に言うとSUBMITしても何も起きない。 この瞬間にCtrl+Shift+Iを押下して、検証(Inspection)ペインを表示させると、以下のエラーが表示された。 exec:1 A different origin subframe tried to create a JavaScript dialog. This is no longer allowed and was blocked. See https://www.chromestatus.com/feature/5148698084376576 for more details. ということで、JavaScriptのダイアログを表示しようとしていますがこれはcloss originなのでもう許せませんみたいな感じ。 GAS、alert、クロスオリジンでググると、これはCORSという問題であると分かった。 SUBMITボタンをクリックすると「本当にサブミットしていいですか[OK][キャンセル]」みたいな確認ダイアログを表示したいという局面があった。 これを、ぼくはサーバーサイドのGAS(ファイル名はCode.jsにしている)ではなくて、クライアントサイドのJavaScript(おなじくjs.js)にwindow.confirm()で書いていた。なぜそうする必要があったかは諸事情で書けない。 あと、必要な項目を入れずにSUBMITした場合も「xxxを入れてからSUBMITしてください[OK]」みたいなエラーダイアログを表示していたが、これもjs.jsでalert()を出していた。 上のURL先の文章によると、これがクロスオリジンiframeだからそういう関数を取れという。 取れ、と言われても機能性が変わってしまうのだが、今回は時間がなかったので全部取ってみた。具体的に言うと let ok = window.confirm("いいんですか?"); if (OK) { いいときの処理... みたいなコードを見つけては //let ok = window.confirm("いいんですか?"); let ok = true; if (OK) { いいときの処理... にしたり、 alert ("ダメですよ"); を見つけては //alert ("ダメですよ"); にするみたいな簡単なお仕事だ。 必要ないんだからブチ消してもいい気がするけど、あとでもっとちゃんと対応するつもりなので、あえてこういう傷跡を残す編集にしておいた。 claspの操作で多少戸惑ったが、自分のQiitaの過去記事で多少役立った。 だからクソ記事でも書いておいた方がいいよ。 Web記事は役に立たないこともある 話が前後するが「CORS Chrome 動かない」でググると、Chromeにオプションを渡して、制限を緩めてやればいい、という記事に当たる。 年々厳しくなってるらしくて、昔の記事のオプションではダメだったけど、こういう風に変えたら動いた、みたいな記事もある。 ということで、制限は日々きつくなっている。 現状は、Chromeにオプションを渡す方法では、今回のエラーは回避できなかった。 今後 ということで、操作性が変わってしまった。 いぜんはSUBMITを押してもワンクッションあったのだが、今回はいきなり実行される。 それでいいかどうかは分からない。 あと、エラーメッセージが出ないのは不親切だから、そのうち対応したい。 対応したいところではあるんだけど、どうしたらいいんだろうね。 JSONPというのを使えばいい? のかな? まあ、そのうちな。 教訓としては、とりあえず安定稼働したシステムでも月1ぐらいは「糠床をかき回す」ぐらいのことをしておいたほうがいいかもしれないということだ。 GASが本業ではないので、放っていたが、クラウドあるあるで、Google様の事情で急に変わっていて、あわてて対応しようとすると必要以上に苦労する。 今後気をつけます。 (この項終わり)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.js + Expressサーバーのポートを変更する

初期設定ではlocalhost:3000となっているが、 bin/wwwファイルを変更することでポートを変更できる。 この点を var port = normalizePort(process.env.PORT || '3000'); このように var port = normalizePort(process.env.PORT || '4000'); これでローカルでwebサーバーを立ち上げる際にポートを分けることができる。 参考 stackoverflow
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactでCSSの条件式適用

三項演算子を使う 例)ダークモードにする export default function App() { const { isDarkMode } = useDarkMode(); return ( <main className={`body ${isDarkMode ? "body-dark" : "body-light"}`}> <Routes /> </main> ); } cssのクラスを関数(useDarkMode())で真偽値を変更させる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【GAS】google formの自動返信メールをHTMLで送信する

目的 Google formの自動返信をGASを使って行う方法はたくさん記事ができているのものの、 ・HTML形式でメール送信 ・送信元を指定する の2つの要素を包含できているものがなかったので、備忘がてらまとめておく。 基本のやり方 スクリプトエディタの開き方や、トリガーの設定方法などは、既存記事にあるので、ここは省略。 下記参照: 【簡単!】Googleフォーム送信時にGASを自動実行する方法 https://www.yukibnb.com/entry/form_trigger HTML形式での送信のやり方 コード.gsファイルと、message.htmlファイルをスクリプトエディタのなかにファイルを作成 コード.gsの例 コード.gs function onFormSubmit(e) { // フォームの回答を取得 回答順で指定する var email = e.values[1]; //メールアドレス var companyName = e.values[2]; //法人名  //使用するhtmlファイルを指定 var html = HtmlService.createHtmlOutputFromFile("message").getContent(); //メールの設定 let to = email; let subject = "アンケート回答の御礼"; //件名 let body = 'htmlメールが表示できませんでした'; //message.htmlが表示できない時の予備 let options = { from: '~~~~~~~~~~', //送り元(必要に応じてカスタム) cc: '~~~~~~~~~, ~~~~~~~~', //CCの設定(必要に応じてカスタム) htmlBody: html } ///メール送信 MailApp.sendEmail( to, subject, body, options ); } message.htmlの例 これがメールの本文になる。 htmlの書き方がわからない人も、下記のようなテキスト→html変換の無料オンラインサービスを使って コピペするだけでOK message.html <!DOCTYPE html> <html> <head> <base target="_top"> </head> <body> <p>いつも大変お世話になっております。<br> カスタマーサポートでございます。</p> <p>この度はお忙しい中アンケートにご協力いただきまして、<br> 誠にありがとうございました。</p> <p>いただきましたご意見、ご要望を、<br> 今後のサービスや業務改善に役立てて参ります。</p> <p>今後ともご愛顧賜りますよう、よろしくお願い申し上げます。</p> <p><br> </p> </body> </html> 注意事項 ・トリガーの設定はお忘れなく! メールの送信元の注意 fromで指定したメールアドレスに関しては、 基本的に自分のgoogleアカウントにエイリアスの通ったメールアドレスしか設定できないので、 送信元にしたいアカウントで、GASを用意してトリガーの設定をする必要がある。 参考URL:https://rtomura-taxacc.com/gas_specify-sender-address-and-sender-name/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript配列の基本

配列の基本 配列の各要素には0から始まる数字(添字、インデックス)によってアクセスできる。そのため、オブジェクトと異なり順序を持っている。 配列には異なる型の要素を入れることができる。 lengthというプロパティで配列の要素数を知ることができる。 配列の最後の要素の添字よりも大きな添字を使って代入を行うと、配列が自動的に大きくなり、値が指定されていない要素にはundefinedが暗黙のうちに代入される。 // 配列リテラル const arr1 = [1, 2, 3]; // 数字の配列 const arr2 = ["one", 2, "three"]; // 異なる型の要素をもつ配列 const arr3 = [[1, 2, 3], ["one", 2, "three"]]; // 配列を要素とする配列 const arr4 = [ // オブジェクトや配列、関数など異なる型を要素としてもつ配列 { name: "サル", type: "object", luckyNumbers: [5, 7, 13] }, [ { name: "キジ", type: "object" }, { name: "鬼", type: "object" }, ], 1, function() { return "配列の要素として関数を記憶することもできる"; }, "three", ]; // 要素へのアクセス console.log(arr1[0]); // 1 console.log(arr1[2]); // 3 console.log(arr3[1]); // ['one', 2, 'three'] console.log(arr4[1][0]); // { name: 'キジ', type: 'object' } // 配列の長さ console.log(arr1.length); // 3 console.log(arr4.length); // 5 console.log(arr4[1].length); // 2 // 配列の長さを大きくする arr1[4] = 5; console.log(arr1); // [1, 2, 3, , 5] console.log(arr1[3]); // undefined console.log(arr1.length); // 5 // 配列の最後の要素より後ろの添字を使う(代入ではない)->大きさは変わらない console.log(arr2[10]); // undefined console.log(arr2.length); // 3 // Arrayコンストラクタ(通常は使われない) const arr5 = new Array(); // 空の配列を生成 console.log(arr5); // [] const arr6 = new Array(1, 2, 3); console.log(arr6); // [ 1, 2, 3 ] const arr7 = new Array(2); // 長さ2の配列(各要素はundefined) console.log(arr7); // [ , ] console.log(arr7[1]); // undefined const arr8 = new Array("2"); console.log(arr8); // ['2'] 配列要素の操作 注意が必要なのは、配列内の要素を変更してしまう「破壊的な」メソッドと新しい配列を返すメソッドの違いです。例えば、pushは配列を変更してしまいますが、concatは新しい配列を返します。 先頭あるいは最後の要素に対する操作 配列arrの先頭要素のことをarr[0]、最後の要素とはarr[arr.length-1]のことを指します。全て破壊的なメソッドです。 push:配列の最後に要素を追加 pop:配列の最後の要素を削除 unshift:先頭に要素を追加 shift:先頭の要素を削除 pushとunshiftの戻り値は変更後の配列の長さです。一方、popとshiftの戻り値は削除された要素です。 let arr = ["b", "c", "d"]; console.log(arr.push("e")); // 4 ←現在の長さ(要素数) console.log(arr); // [ 'b', 'c', 'd', 'e' ] console.log(arr.pop()); // e console.log(arr); // [ 'b', 'c', 'd' ] console.log(arr.unshift("a")); // 4 ←現在の長さ console.log(arr); // [ 'a', 'b', 'c', 'd' ] console.log(arr.shift()); // a console.log(arr); // [ 'b', 'c', 'd' ] 複数要素の追加 concat:複数の要素を配列に追加し、配列のコピーを戻す。 非破壊的なメソッドです。 let arr = [1, 2, 3]; let arr2 = arr.concat(4, 5, 6); console.log(arr); // [ 1, 2, 3 ] (← 変更なし。以降も同じ) console.log(arr2); // [ 1, 2, 3, 4, 5, 6 ] arr2 = arr.concat([4, 5, 6]); // (配列を渡す) console.log(arr2); // [ 1, 2, 3, 4, 5, 6 ] arr2 = arr.concat([4, 5], 6); console.log(arr2); // [ 1, 2, 3, 4, 5, 6 ] arr2 = arr.concat([4, 5], [6, 7]); // 引数は2つでいずれも配列 console.log(arr2); // [ 1, 2, 3, 4, 5, 6, 7 ] arr2 = arr.concat([4, [5, 6]]); // 引数は配列ひとつでその2番目の要素が配列 console.log(arr2); // [ 1, 2, 3, 4, [ 5, 6 ] ] 部分配列 slice:ある配列の部分からなる配列を作るメソッド。 非破壊的なメソッド let arr = [11, 12, 13, 14, 15]; let arr2 = arr.slice(3); // arr[3]から後ろ console.log(arr2); // [ 14, 15 ] console.log(arr); // [ 11, 12, 13, 14, 15 ] (変更なし。以降も同じ) arr2 = arr.slice(2, 4); // arr[2]からarr[4]のひとつ前まで console.log(arr2); // [ 13, 14 ] arr2 = arr.slice(-2); // 最後から2番目以降 console.log(arr2); // [ 14, 15 ] arr2 = arr.slice(1, -2); // arr[1]から、最後から2番目のひとつ前まで console.log(arr2); // [ 12, 13 ] arr2 = arr.slice(-2, -1); // 最後から2番目から最後から1番目のひとつ前まで console.log(arr2); // [ 14 ] 途中の要素の削除や途中への要素の追加 splice:配列の任意の場所を指定して内容を変更することができるメソッド。第一引数が変更を開始する場所、第二引数は削除する要素の数、第三引数以降は追加する要素を指定する。 破壊的なメソッド let arr = [1, 5, 7]; let arr2 = arr.splice(1, 0, 2, 3, 4); // arr[1]から2, 3, 4が追加される console.log(arr); // [ 1, 2, 3, 4, 5, 7 ] console.log(arr2); // [] ←何も削除されていない arr2 = arr.splice(5, 0, 6); // arr[5]に6が追加されて、以降ひとつずつ後ろへ console.log(arr); // [ 1, 2, 3, 4, 5, 6, 7 ] console.log(arr2); // [] ←何も削除されていない arr2 = arr.splice(1, 2) // arr[1]から2個削除 console.log(arr); // [ 1, 4, 5, 6, 7 ] console.log(arr2); // [ 2, 3 ] ←削除された要素 arr2 = arr.splice(2, 1, 'a', 'b'); // arr[2]から1個削除して'a'と'b'をそこに追加 console.log(arr); // [ 1, 4, 'a', 'b', 6, 7 ] console.log(arr2); // [ 5 ] ←削除された要素 逆転とソート reverse:配列の要素を逆順に並び替えるメソッド。 破壊的なメソッド let arr = [1, 2, 3, 4, 5]; let arr2 = arr.reverse(); console.log(arr); // [ 5, 4, 3, 2, 1 ] console.log(arr2); // [ 5, 4, 3, 2, 1 ] ← reverseはオブジェクト自身を返す arr.reverse(); console.log(arr); // [ 1, 2, 3, 4, 5 ] console.log(arr2); // [ 1, 2, 3, 4, 5 ] sort:配列の要素のソートを行うメソッド。基本は昇順。 破壊的なメソッド let arr = [5, 3, 2, 4, 1]; let arr2 = arr.sort(); console.log(arr); // [ 1, 2, 3, 4, 5 ] console.log(arr2); // [ 1, 2, 3, 4, 5 ] arr2.reverse(); console.log(arr); // [ 5, 4, 3, 2, 1 ] console.log(arr2); // [ 5, 4, 3, 2, 1 ] sortはソートの時に使う関数を指定することもできます。 let arr = [{ name: "Suzanne" }, { name: "Jim" }, { name: "Trevor" }, { name: "Amanda" }]; console.log(arr); arr.sort((a, b) => a.name > b.name); // nameでソート console.log("------"); console.log(arr); arr.sort((a, b) => a.name[1] < b.name[1]); // nameの2文字目で逆順にソート console.log("------"); console.log(arr); /* 実行結果 [ { name: 'Suzanne' }, { name: 'Jim' }, { name: 'Trevor' }, { name: 'Amanda' } ] ------ [ { name: 'Amanda' }, { name: 'Jim' }, { name: 'Suzanne' }, { name: 'Trevor' } ] ------ [ { name: 'Suzanne' }, { name: 'Trevor' }, { name: 'Amanda' }, { name: 'Jim' } ] */ 検索 indexOf:引数に指定した値に厳密に等しい要素をもつ最初の添字を返す。見つからなかった場合は-1を返す。 lastIndexOf:同様の最後の添字を返す。見つからなかった場合は-1を返す。 const o = { name: "ジェリー" }; const arr = [1, 5, "a", o, true, 5, [1, 2], "9"]; console.log(arr.indexOf("a")); // 2 console.log(arr.lastIndexOf("a")); // 2 console.log(arr.indexOf({ name: "ジェリー" })); // -1 console.log(arr.indexOf("a", 5)); // -1 ("a"をarr[5]から検索する) console.log(arr.indexOf(5, 5)); // 5 console.log(arr.lastIndexOf(5, 4)); // 1 (arr[4]から左に探す) console.log(arr.lastIndexOf(true, 3)); // -1 (arr[3]から左に探してもない) findIndex:indexOfと似ているが、比較の際に用いる関数を指定することができる。 const arr = [{ id: 5, name: "太郎" }, { id: 7, name: "花子" }]; console.log(arr.findIndex(element => element.id === 5)); // 0 ←idが5ならば条件にマッチ console.log(arr.findIndex(element => element.name === "花子")); // 1 console.log(arr.findIndex(element => element === 3)); // -1 console.log(arr.findIndex(element => element.id === 17)); // -1 console.log(arr.findIndex(element => element.id === 7)); // 1 find:比較に用いる関数を指定する。要素自体が返ってくる。 const arr = [{ id: 5, name: "太郎" }, { id: 7, name: "花子" }]; console.log(arr.find(element => element.id === 5)); // { id: 5, name: '太郎' } console.log(arr.find(element => element.id === 2)); // undefined map 配列内の要素を変換する。 非破壊的なメソッド const cart = [ { 名前: "iPhone", 価格: 54800}, { 名前: "Android", 価格: 49800}]; const names = cart.map(element => element.名前); // 各オブジェクトの「名前」からなる配列を新たに作る console.log(names); // [ 'iPhone', 'Android' ] const prices = cart.map(element => element.価格); console.log(prices); // [ 54800, 49800 ] const discountPrices = prices.map(element => element*0.8); // 2割引の価格 console.log(discountPrices); // [ 43840, 39840 ] const lcNames = names.map(element => element.toLowerCase()); // 小文字にする // const lcNames = names.map(String.toLowerCase); // ↑Firefoxではこれでも動くが、nodeやGoogle Chromeでは動作しない console.log(lcNames); // [ 'iphone', 'android' ] 次の例では2つの配列を合体してオブジェクトを要素とする配列を作っている。 const items = ["iPhone", "Android"]; const prices = [54800, 49800]; const cart = items.map((x, i) => ({ 名前: x, 価格: prices[i]})); console.log(cart); // [ { '名前': 'iPhone', '価格': 54800 }, { '名前': 'Android', '価格': 49800 } ] filter 配列から不要な要素を取り去る働きをする。条件にマッチしない要素が削除された配列を返す。 非破壊的なメソッド const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present']; const result = words.filter(word => word.length > 6); console.log(result); // expected output: Array ["exuberant", "destruction", "present"] reduce 配列全体を変換する。mapは配列の各要素を変換するが、reduceは配列全体を変換する。 例えば、配列の全要素の合計を計算したり、平均を計算したりして一つの値に変換する。 reduceに渡される関数は2つの引数を取る。一つはアキュムレータ(a)で、もう一つは現在の配列要素(x)。 const arr = [5, 7, 2, 4]; const sum = arr.reduce((a, x) => a += x, 0); console.log(sum); // 18 const sum2 = arr.reduce((a, x) => a + x, 0); /* 「+=」の「=」 は省略できる */ console.log(sum2); // 18 join 配列の各要素をまとめて、1つの文字列を作る。第一引数はセパレータ(デフォルトは「,」になっている)。 非破壊的なメソッド const arr = [1, null, "hello", "world", true, undefined]; delete arr[3]; let result = arr.join(); console.log(result); // 1,,hello,,true, result = arr.join(''); console.log(result); // 1hellotrue result = arr.join(' -- '); console.log(result); // 1 -- -- hello -- -- true -- 参考文献 初めてのJavaScript 引用コード
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript / jQuery / 非同期処理】「$(async function() {...})」と書いて良いのは jQuery 3.3系 以降っぽい

前段 $(function() {...})内で、ある非同期処理を await で待機する処理を書こうとしてハマった話です。 結論 $(async function() {...})と書いて良いのは jQuery 3.3系以降のようです。 それ以前のバージョンの jQuery を使用しているなら document.addEventListener('DOMContentLoaded', async function() {...} で代替するしかなさそうです。 内容 await を記述するために$(async function() {...}と書いたのですが、実行させるとそもそもこの関数自体が実行されなくなってしまいました。 もちろん、以下のように await を直接書いてしまうとUncaught SyntaxError: await is only valid in async functions and the top level bodies of modulesのエラーが発生します。 function resolveAfter2Seconds() { return new Promise(resolve => { setTimeout(() => { resolve('resolved'); }, 2000); }); } $(function() { const result = await resolveAfter2Seconds(); console.log(result); }); 調べると、$(async function() {...}を認識してくれるのは jQuery 3.3系以降のようでした。 Google Hosted Libraries で1つずつ確認しました。(3.3.0 はCDNに存在しなかったようなので未調査) バージョン 結果 1.X系 ☓ 2.X系 ☓ 3.2.0 ☓ 3.3.1 ○ 3.4.0 ○ ※jQuery 3.X 系のバージョンアップ情報を探しても正確な情報は見つけられなかったのですが、 実際に src のバージョンを書き換えて1つずつ実行してみた結果が前述の表になります。 ※@ko1nksm さんより情報頂きました。ありがとうございます https://qiita.com/shimamura_io/items/57e18f11d3cc0faaefce#comment-80979316e737ee64bef8
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでInputフィールドにバリデーションをつけよう(初級編)

前提知識 Reactの基礎(useState, useEffect) Next.jsの基礎 TypeScriptの基礎 対象 JavaScriptは理解できている React初学者 使用技術 Next.js TypeScript Sass この記事で行うこと 今回はReactを用いてinputフィールドにフロント側からバリデーションをかけていきます。 今回は初級編と応用編に分けておこなっていきます! 初級編はまずはtodoアプリを用いてinputフィールドを作成し、 応用編ではinputフィールドをcomponentに落とし込んで再利用可能にしていきます。 さっそくやってみる まずゴールとしては簡単なtodoを作成するだけにするので inputフィールドにタスクを追加する 追加ボタンを押す todoエリアに追加される こちらの基本的な工程から行っていきます。 ※今回はNext.jsを使用しています。 pages/input/index.tsx import styled from "./input.module.scss" export default function Input() { return ( <> <h1 className={styled.heading}>インプットフィールドの練習</h1> </> ); }; pages/input/input.module.scss .heading { font-size: 30px; font-weight: bold; } まず簡単に二つのファイルを作成しましょう。 ではこちらにTodoの追加フローを足していきます。 コードを書いた後に説明していきます。 pages/input/index.tsx import styled from "./input.module.scss" import React, {useState} from "react"; export default function Input() { const [ todoText, setTodoText ] = useState(""); const [ todos, setTodos ] = useState([]); const onChangeTodoText = (event: React.ChangeEvent<HTMLInputElement>): void => setTodoText(event.target.value); const onClickAddTodoText = (): void => { const newArrayTodos = [...todos, todoText]; setTodos(newArrayTodos); setTodoText(""); } return ( <> <h1 className={styled.heading}>インプットフィールドの練習</h1> <div className={styled.inputArea}> <input className={styled.todoInputField} placeholder={"todoを入力してください"} type={"text"} value={todoText} onChange={onChangeTodoText} /> <button className={styled.addTodoButton} onClick={onClickAddTodoText} >todoを追加</button> </div> <div className={styled.todoArea}> <h2 className={styled.heading2}>Todoリスト</h2> {todos.length === 0 ? ( <p className={styled.md10}>todoはまだ登録されていません</p> ) : ( <ul className={styled.md10}> {todos.map((todo) => ( <li key={todo}>{ todo }</li> ))} </ul> ) } </div> </> ); }; pages/input/input.module.scss .heading { font-size: 30px; font-weight: bold; } .heading2 { font-size: 24px; font-weight: bold; margin-top: 20px; } .md10 { margin-top: 10px; } .inputArea { margin-top: 20px; .todoInputField { margin-right: 5px; } } こちらが表示されたかと思います。 では簡単に説明していきます。 <input className={styled.todoInputField} placeholder={"todoを入力してください"} type={"text"} value={todoText} onChange={onChangeTodoText} /> まずはこちらからみていきましょう。 inputのtypeはtextにし、valueでtodoTextを受け取っています。 todoTextは const [ todoText, setTodoText ] = useState(""); こちらでstateとして定義しています。 初期値は空文字列なのでinputの中身はplaceholderが表示されている状態です。 このままではvalueが空文字で設定されているので文字を入力することができません。 ここでonChangeイベントを使っています。 onChangeはinputフィールドに変更があった時に反応するイベントです。 ここでonChangeTodoTextの関数が発火します! const onChangeTodoText = (event: React.ChangeEvent<HTMLInputElement>): void => setTodoText(event.target.value); こちらですね。 inputフィールドはeventを引数に受け取ります。 これはもうお決まり事項なので暗記しましょう。 eventの型もお決まりといった形なのですが、これは覚えるというよりかは都度eventの型はこんな感じかというのを把握して都度ググるといった感じで大丈夫だと思います。 setTodoTextでtodoTextの初期値を更新しています。 event.target.valueでinputフィールドに入力された値を受け取っています! こちらもお決まりなので暗記です!! これでinputフィールドが入力できるようになっています。 <button className={styled.addTodoButton} onClick={onClickAddTodoText} >todoを追加</button> todoの追加ボタンでClickイベントを発火させています。 const onClickAddTodoText = (): void => { const newArrayTodos = [...todos, todoText]; setTodos(newArrayTodos); setTodoText(""); } newArrayTodosで新しい配列を生成しております。 const newArrayTodos = [...todos, todoText]; こちらはスプレット構文です。 [...もとの配列, 追加する実態] = 新しい配列 といったイメージです。 昔は.pushなどで配列にボコボコ追加していくスタイルが主流でしたが、pushなどは破壊的メソッドで少し危険な部分があるため、基本的にはスプレット構文を使うことをおすすめいたします。 setTodos(newArrayTodos); setTodoText(""); 新しく生成した配列をsetTodosで空の初期値の配列を更新しています。 const [ todos, setTodos ] = useState([]); 初期値はstateで定義してあります。 さらに最後にinputフィールドから追加された際はinputフィールドから文字は消えてほしいためsetTodoTextを空文字に更新しています。 <div className={styled.todoArea}> <h2 className={styled.heading2}>Todoリスト</h2> {todos.length === 0 ? ( <p className={styled.md10}>todoはまだ登録されていません</p> ) : ( <ul className={styled.md10}> {todos.map((todo) => ( <li key={todo}>{ todo }</li> ))} </ul> ) } </div> 追加の処理がかけたのであとはtodoを表示していきます。 三項演算子を活用し todo.length === 0 ?のとき、つまり配列の数が0のとき "todoはまだ登録されていません"を表示しています。 逆にtodoが登録された場合は {todos.map((todo) => ( <li key={todo}>{ todo }</li> ))} mapで配列を回しています。 こちらでtodo作成するフローは終了です。 バリデーションをかけていく 今回は - Todoは0文字では追加できない - Todoは10文字以内 といったバリデーションを練習としてかけていきます。 ではファイルを更新していきましょう。 こちら一応今回の完成系のコードになります。 では一つずつ確認していきましょう。 pages/input/index.tsx import styled from "./input.module.scss" import React, {useEffect, useState} from "react"; export default function Input() { const [ todoText, setTodoText ] = useState<string>(""); const [ todos, setTodos ] = useState<Array<string>>([]); const [ isError, setIsError ] = useState<boolean>(false); const [ errorMessage, setErrorMessage ] = useState<string>(""); const todoLengthZero = (): boolean => { if (todoText.length === 0) return true; } const onChangeTodoText = (event: React.ChangeEvent<HTMLInputElement>): void => { if (todoText.length >= 10) { setIsError(true) setErrorMessage("Todoは10文字いないで入力してください") } else { setIsError(false) } setTodoText(event.target.value); } const onClickAddTodoText = (): void => { const newArrayTodos = [...todos, todoText]; setTodos(newArrayTodos); setTodoText(""); } useEffect((): void => { todoLengthZero() }, []) return ( <> <h1 className={styled.heading}>インプットフィールドの練習</h1> <div className={styled.inputArea}> {isError && <p className={styled.errorMessage}>{ errorMessage }</p>} <input className={styled.todoInputField} placeholder={"todoを入力してください"} type={"text"} value={todoText} onChange={onChangeTodoText} /> <button className={styled.addTodoButton} onClick={onClickAddTodoText} disabled={isError || todoLengthZero()} >todoを追加</button> </div> <div className={styled.todoArea}> <h2 className={styled.heading2}>Todoリスト</h2> {todos.length === 0 ? ( <p className={styled.md10}>todoはまだ登録されていません</p> ) : ( <ul className={styled.md10}> {todos.map((todo) => ( <li key={todo}>{ todo }</li> ))} </ul> ) } </div> </> ); }; まずエラーメッセージの定義からです。 {isError && <p className={styled.errorMessage}>{ errorMessage }</p>} こちらでエラーがある場合のみエラーメッセージを表示するように設定しています。 エラーのフラグはisErrorで定義していますが、 const [ isError, setIsError ] = useState<boolean>(false); こちらはstateで初期値をfalseで定義しています。 なのでデフォルトはエラ〜メッセージが表示されません。 その下のinputフィールドをみていきましょう。 <input className={styled.todoInputField} placeholder={"todoを入力してください"} type={"text"} value={todoText} onChange={onChangeTodoText} /> こちらはonChange(フィールドに入力された際に発火)イベントでonChangeTodoTextの関数を呼び出しています。 const onChangeTodoText = (event: React.ChangeEvent<HTMLInputElement>): void => { if (todoText.length >= 10) { setIsError(true) setErrorMessage("Todoは10文字いないで入力してください") } else { setIsError(false) } setTodoText(event.target.value); } こちらです。 内容としてはtodo.length >= 10(todoの文字数が10以上であれば)setIsErrorをtrueにし setErrorMessageを入力します。 const [ isError, setIsError ] = useState<boolean>(false); const [ errorMessage, setErrorMessage ] = useState<string>(""); こちらで初期値が定義されていたので変わります。 ここでisErrorがtrueになるため {isError && <p className={styled.errorMessage}>{ errorMessage }</p>} 先ほどのerrorMessageが表示されるようになります。 こちらで10文字以上のエラーメッセージの表示は完了です。 次は0文字のバリデーションは初めからエラーメッセージが表示されるのは少しうざいのでdisabledを処理していきます。 <button className={styled.addTodoButton} onClick={onClickAddTodoText} disabled={isError || todoLengthZero()} >todoを追加</button> disabled={isError || todoLengthZero()}まずこちらの処理でisErrorがtrueになっている場合、つまり10文字以上の文字が入力されている場合はtrueが入ります。 それとtodoLengthZoroの関数をここで呼んでいます。 const todoLengthZero = (): boolean => { if (todoText.length === 0) return true; } こちらはtodoの文字が0と等しければtrueを返す関数です。 つまり0文字以上ではボタンにdisabledにtrueが入り、ボタンが押せなくなるのです。 こちらをまとめると disabled = { 文字数が10文字以上の場合はtrue または 文字数が0文字と等しい場合はtrue } になります! ただこれだけだと、初期ロード時には0文字でもボタンがdisabledになりません。 なぜなら todoLengthZeroの関数が呼ばれないからです。 こちらを改善するのがuseEffectです useEffect((): void => { todoLengthZero() }, []) useEffectはマウント時に処理を走らせます。 ちなみにreturnを記載するとアンマウント時の処理になります。 つまり初期ロードが走りマウント時に関数が走るためdisabledが有効になります。 これでバリデーションの処理が完了になります。 最後にスタイルを少し整えましょう。 .heading { font-size: 30px; font-weight: bold; } .heading2 { font-size: 24px; font-weight: bold; margin-top: 20px; } .md10 { margin-top: 10px; } .inputArea { margin-top: 20px; .todoInputField { margin-right: 5px; } } .errorMessage { color: red; } こちらで初級編は終了になります。 次に応用編ですが今回のコードをリファクタリグして、保守性の高いコードに近づけていきます。 お疲れさまでした!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む