tag:crieit.net,2005:https://crieit.net/tags/JavaScript/feed 「JavaScript」の記事 - Crieit Crieitでタグ「JavaScript」に投稿された最近の記事 2024-04-27T13:37:45+09:00 https://crieit.net/tags/JavaScript/feed tag:crieit.net,2005:PublicArticle/18824 2024-04-27T13:37:04+09:00 2024-04-27T13:37:45+09:00 https://crieit.net/posts/truncate-string-by-javascript 【JS】テキストを一定の文字数で切り捨てる <p>こんにちは、しきゆらです。<br /> 今回は、SNSなどで時々見るような「一定数を超えたテキストを切り捨てる」処理を作ったのでメモしておきます。</p> <h2 id="結論: 以下のコードでいい感じに端折れる"><a href="#%E7%B5%90%E8%AB%96%3A+%E4%BB%A5%E4%B8%8B%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89%E3%81%A7%E3%81%84%E3%81%84%E6%84%9F%E3%81%98%E3%81%AB%E7%AB%AF%E6%8A%98%E3%82%8C%E3%82%8B">結論: 以下のコードでいい感じに端折れる</a></h2> <pre><code class="js">const truncate = (str, max) => { if (str.length < max) return str; return `${str.slice(0, max - 1)}…`; }; const truncateStringExceptURL = (str, max) => { if (!str) return ""; // URLの正規表現 const urlRegexp = /https?:\/\/\S+/g; // マッチしたURLリスト const urls = []; // 文字列からURLにマークして除外する const markedString = str.replace(urlRegexp, (matchStr) => { urls.push(matchStr); return `>${urls.length - 1}<`; }); // テキストを切り捨てる const truncatedString = truncate(markedString, max); // マークしたURL箇所の正規表現 const replaceUrlRegexp = />\d+</; // マークをURLに置換 return truncatedString.replace(replaceUrlRegexp, (matchStr) => { const urlIndex = parseInt(matchStr.replace(/>|</, ""), 10); return urls[urlIndex]; }); }; </code></pre> <p>細かく見ていきます。</p> <h2 id="テキストを切り捨てる"><a href="#%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%92%E5%88%87%E3%82%8A%E6%8D%A8%E3%81%A6%E3%82%8B">テキストを切り捨てる</a></h2> <p>単純に一定の文字数で切り捨てるだけであれば、以下だけで実現できます。<br /> なお、ここでは文字数をString.lengthで簡易的に取得していますが、文字コード等によっては意図しない数になりそうです。<br /> 正確な文字数に関しては、以下が参考になると思います。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/suin/items/3da4fb016728c024eaca">JavaScript: 文字数を正確にカウントするには? #JavaScript - Qiita https://qiita.com/suin/items/3da4fb016728c024eaca</a></p> <pre><code class="js">const truncate = (str, max) => { if (str.length < max) return str; return `${str.slice(0, max - 1)}…`; }; </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/slice">String.prototype.slice() - JavaScript | MDN https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/slice</a></p> <p>この中で、URLが入っている場合はURLの途中で切り捨てられてしまいます。<br /> それが問題にならなければよいのですが、私の場合はURLはそのまま出した方が便利だったのでURL以外の部分で切り捨てるようにしてみた結果が、最初のコードでした。</p> <p>処理の流れとしては、以下の通り。</p> <ul> <li>文字列に含まれるURLを特定のマーカに置換</li> <li>マーカに置換した文字列を指定された文字数に切り捨て</li> <li>切り捨てられた文字列にあるURLマーカをURLに戻す</li> </ul> <p>それぞれ見てみます。</p> <h3 id="文字列に含まれるURLを特定のマーカに置換"><a href="#%E6%96%87%E5%AD%97%E5%88%97%E3%81%AB%E5%90%AB%E3%81%BE%E3%82%8C%E3%82%8BURL%E3%82%92%E7%89%B9%E5%AE%9A%E3%81%AE%E3%83%9E%E3%83%BC%E3%82%AB%E3%81%AB%E7%BD%AE%E6%8F%9B">文字列に含まれるURLを特定のマーカに置換</a></h3> <p>該当部分を抜き出します。</p> <pre><code class="js">const truncateStringExceptURL = (str, max) => { ... // URLの正規表現 const urlRegexp = /https?:\/\/\S+/g; // マッチしたURLリスト const urls = []; // 文字列からURLにマークして除外する const markedString = str.replace(urlRegexp, (matchStr) => { urls.push(matchStr); return `>${urls.length - 1}<`; }); ... } </code></pre> <p>URLを表現する正規表現を定義し、文字列内にあれば配列に格納しつつマーカに保管しています。</p> <p><code>String.prototype.replace()</code>は第1引数に検知するパターン、第2引数に置換するものを渡しますが、第2引数は文字列以外にも関数も渡すことができます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/replace">String.prototype.replace() - JavaScript | MDN https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/replace</a></p> <p>なお、この処理自体は過去記載した「YAMLに環境変数を埋め込む」や「YAMLにローカル変数を埋め込む」と同じ発想です。</p> <p><a target="_blank" rel="nofollow noopener" href="https://shikiyura.com/2021/03/embed_env_value_in_yaml/">【Ruby】YAMLに環境変数を埋め込む | しきゆらの備忘録 https://shikiyura.com/2021/03/embed_env_value_in_yaml/</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://shikiyura.com/2021/10/embed_local_variable_in_yaml/">【Ruby】YAMLにローカル変数を埋め込む | しきゆらの備忘録 https://shikiyura.com/2021/10/embed_local_variable_in_yaml/</a></p> <h3 id="マーカに置換した文字列を指定された文字数に切り捨て"><a href="#%E3%83%9E%E3%83%BC%E3%82%AB%E3%81%AB%E7%BD%AE%E6%8F%9B%E3%81%97%E3%81%9F%E6%96%87%E5%AD%97%E5%88%97%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%95%E3%82%8C%E3%81%9F%E6%96%87%E5%AD%97%E6%95%B0%E3%81%AB%E5%88%87%E3%82%8A%E6%8D%A8%E3%81%A6">マーカに置換した文字列を指定された文字数に切り捨て</a></h3> <p>該当部分を抜き出します。</p> <pre><code class="js">const truncate = (str, max) => { if (str.length < max) return str; return `${str.slice(0, max - 1)}…`; }; const truncateStringExceptURL = (str, max) => { ... // テキストを切り捨てる const truncatedString = truncate(markedString, max); ... } </code></pre> <p>URLをマーカに置換した文字列に対して、指定数以上の文字列を切り捨てる処理をかませます。</p> <p>単純に<code>String.prototype.slice()</code>で先頭から最大数-1までの文字列を作り、末尾に…を入れてます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/slice">String.prototype.slice() - JavaScript | MDN https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/slice</a></p> <h3 id="切り捨てられた文字列にあるURLマーカをURLに戻す"><a href="#%E5%88%87%E3%82%8A%E6%8D%A8%E3%81%A6%E3%82%89%E3%82%8C%E3%81%9F%E6%96%87%E5%AD%97%E5%88%97%E3%81%AB%E3%81%82%E3%82%8BURL%E3%83%9E%E3%83%BC%E3%82%AB%E3%82%92URL%E3%81%AB%E6%88%BB%E3%81%99">切り捨てられた文字列にあるURLマーカをURLに戻す</a></h3> <p>該当箇所を抜き出します。</p> <pre><code class="js">const truncateStringExceptURL = (str, max) => { ... // マークしたURL箇所の正規表現 const replaceUrlRegexp = />\d+</; // マークをURLに置換 return truncatedString.replace(replaceUrlRegexp, (matchStr) => { const urlIndex = parseInt(matchStr.replace(/>|</, ""), 10); return urls[urlIndex]; }); ... } </code></pre> <p>ここでは、マーカを正規表現で表現し、切り捨てた文字列からマーカ部分をURLに置換しています。<br /> 処理的にはマーカに置き換える処理の反対なので特に複雑なことはないかと。</p> <p>マーカ部分をもう少し綺麗に書けないかな、と思っていますがいい方法思いついてません。<br /> URLをマーカに置換する部分と、マーカからURLに置換するが分かれているので何とかしたい気持ち。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>今回は、SNSなどでよく見るような一定数を超えた文字列を端折る処理を書いたのでメモしました。</p> <p>改めて全体を記載しておきます。</p> <pre><code class="js">const truncate = (str, max) => { if (str.length < max) return str; return `${str.slice(0, max - 1)}…`; }; const truncateStringExceptURL = (str, max) => { if (!str) return ""; // URLの正規表現 const urlRegexp = /https?:\/\/\S+/g; // マッチしたURLリスト const urls = []; // 文字列からURLにマークして除外する const markedString = str.replace(urlRegexp, (matchStr) => { urls.push(matchStr); return `>${urls.length - 1}<`; }); // テキストを切り捨てる const truncatedString = truncate(markedString, max); // マークしたURL箇所の正規表現 const replaceUrlRegexp = />\d+</; // マークをURLに置換 return truncatedString.replace(replaceUrlRegexp, (matchStr) => { const urlIndex = parseInt(matchStr.replace(/>|</, ""), 10); return urls[urlIndex]; }); }; </code></pre> <p>今回は、ここまで。</p> <p>おわり</p> しきゆら tag:crieit.net,2005:PublicArticle/18812 2024-04-16T23:38:37+09:00 2024-04-16T23:38:37+09:00 https://crieit.net/posts/JS-Google-Apps-Script-vite 【JS】Google Apps Scriptのコードをviteでバンドルする <p>こんにちは、しきゆらです。<br /> 今回は、Google Apps Script(GAS)をViteでビルドしてclaspでプッシュする環境を作っていきます。<br /> ・・・久々のプログラム関連の記事な気がする。</p> <p>個人的に、GASのコードをローカルで書き、JSのバンドラー?ビルダー?を使ってビルドしてclaspでアップロードする、というのがGASを書く上でのお作法だと思っています。</p> <p>claspに関しては、こちらをご覧ください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/google/clasp">GitHub - google/clasp: 🔗 Command Line Apps Script Projects https://github.com/google/clasp</a></p> <p>ビルドツールも様々あります。<br /> Webpackやparcel、esbuildなどいろいろあるうえに、時代によって流行り廃りが速いのであまり追い切れてません。<br /> ビルドツールについてはいかがいろいろまとまっててよさそう。</p> <p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/nakaakist/articles/86457bf2908379">JavaScriptビルドツールの整理 各ツールの機能と依存関係</a><br /> <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/mizchi/articles/native-esm-age">Native ESM 時代のフロントエンドビルドツールの動向</a></p> <p>また、過去のツールの流行り廃りは以下あたりが参考になるかと。</p> <p><a target="_blank" rel="nofollow noopener" href="https://stateofjs.com/en-US">State of JavaScript</a></p> <p>ということで、いろいろあるわけですが<br /> 今回は、tauriでちょっと使って動作が速かったviteを使ってGASをビルドする環境を作ってみようと思います。</p> <p><a target="_blank" rel="nofollow noopener" href="https://ja.vitejs.dev/">Vite | 次世代フロントエンドツール</a></p> <p>Tauriを触ったときの記事はこの辺をご覧ください。</p> <p><a target="_blank" rel="nofollow noopener" href="https://shikiyura.com/2023/08/create_simple_app_with_tauri/">【Rust】Tauriで簡易的なアプリを作ってみる</a></p> <p>今回、調べてみるとviteを使ってGASを書く記事はHTMLを作ってWebアプリとしての環境構築ばかりでした。<br /> 欲しいのはJSやTSをビルドするだけの環境です。<br /> 色々調べているとviteの裏側にあるRollupのプラグインでGAS向けのものがあったのでこれを使ってみます。</p> <h2 id="環境構築"><a href="#%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89">環境構築</a></h2> <p>適当なフォルダで以下のツールをインストール。<br /> ここではyarn(v4.1.1)を使ってインストールしていますが、他のツールを使っている場合も同じような感じかと思うので読み替えていただければと。</p> <p><code>yarn add clasp vite rollup-plugin-google-apps-script</code></p> <p>それぞれのリポジトリをいかにおいておきます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/google/clasp">GitHub - google/clasp: 🔗 Command Line Apps Script Projects</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/vitejs/vite">GitHub - vitejs/vite: Next generation frontend tooling. It's fast!</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/mato533/rollup-plugin-gas#readme">GitHub - mato533/rollup-plugin-gas: Rollup plugin for Google Apps Script. This plugin supports local development of applications that run on Google Apps Script.</a></p> <h2 id="vite/rollupの設定"><a href="#vite%2Frollup%E3%81%AE%E8%A8%AD%E5%AE%9A">vite/rollupの設定</a></h2> <p>viteは設定ファイルが必要です。<br /> しかし、必要なものは以下に参考としてあるので、これをもとにちょっと手直しします。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/mato533/rollup-plugin-gas?tab=readme-ov-file#vite">GitHub - mato533/rollup-plugin-gas: Rollup plugin for Google Apps Script. This plugin supports local development of applications that run on Google Apps Script.</a></p> <pre><code class="javascript">// vite.config.js import { defineConfig } from "vite"; import rollupPluginGas from "rollup-plugin-google-apps-script"; import path from "path"; export default defineConfig({ plugins: [rollupPluginGas()], build: { rollupOptions: { input: "src/main.js", output: { dir: "dist", entryFileNames: "main.js", }, }, minify: false, // trueにすると関数名が消えるのでfalse必須 }, resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, }); </code></pre> <p>ここでは<code>src/main.js</code>というファイルを作り、これを<code>dist/main.js</code>へビルドする形を作っていきます。</p> <h2 id="GASのコードを書く"><a href="#GAS%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E6%9B%B8%E3%81%8F">GASのコードを書く</a></h2> <p>では、<code>src/main.js</code>へ適当なコードを書きます。<br /> 単一ファイルだけでなく、せっかくなので分割してimportする形で書いてみます。</p> <pre><code class="javascript">// src/main.js import parts from "./parts"; const greet = () => { Logger.log("hello"); }; global.greet = greet; global.parts = parts; </code></pre> <pre><code class="javascript">// src/parts.js export default parts = () => { Logger.log("parts"); }; </code></pre> <p>では、ビルドします。<br /> ビルドは以下のコマンドでできます。</p> <p><code>yarn run vite build</code></p> <p>ビルドすると以下のようなコードが吐き出されるはず。</p> <pre><code class="javascript">// dist/main.js var global = this; function greet() { } ; function parts() { } ; (function(factory) { typeof define === "function" && define.amd ? define(factory) : factory(); })(function() { "use strict"; const parts$1 = parts = () => { Logger.log("parts"); }; const greet2 = () => { Logger.log("hello"); }; global.greet = greet2; global.parts = parts$1; }); </code></pre> <p>では、コードをGASへアップしましょう。</p> <h2 id="Google Apps Scriptへプッシュする"><a href="#Google+Apps+Script%E3%81%B8%E3%83%97%E3%83%83%E3%82%B7%E3%83%A5%E3%81%99%E3%82%8B">Google Apps Scriptへプッシュする</a></h2> <p>まずは、claspからログインしましょう。<br /> 以下のコマンドを実行すると、ブラウザが立ち上がるので、画面に従ってGoogleアカウントへログインしましょう。</p> <p><code>clasp login</code></p> <p>ログイン後、以下のコマンドでApps Scriptを作成します。 なお、すでに作成済みの場合は場合はcloneでもよいです。</p> <pre><code class="bash">> clasp create ? Create which script? standalon reated new standalone script: <https://script.google.com/d/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/edit> Warning: files in subfolder are not accounted for unless you set a '/path/to/project/.claspignore' file. Cloned 1 file. └─ /path/to/project/appsscript.json </code></pre> <p><code>clasp create</code>を実行するとどんなスクリプトか聞かれるので、今回はstandaloneを指定してます。</p> <p>完了すると、URLが表示されます。<br /> これを開くと、作成されたApps Scriptへアクセスできます。<br /> また、<code>clasp open</code>でも開けます。</p> <p>そして、<code>appsscript.json</code>、<code>.clasp.json</code>の2ファイルが生成されます。<br /> <code>.clasp.json</code>ファイルを開き、rootDirを<code>vite.config.js</code>で設定したdistフォルダを指定します。</p> <p><code>// .clasp.json { "scriptId":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "rootDir":"/path/to/project/dist" }</code></p> <p>そして、<code>appsscript.json</code>ファイルをdistフォルダへコピーします。</p> <p>では、<code>clasp push</code>でコードをApps Scriptへアップロードします。</p> <p><code>clasp push</code></p> <p>Apps Scriptを開くと、以下のようにコードが反映されているはずです。<br /> では、関数名を指定して実行ボタンを押して動作確認しましょう。<br /> <a href="https://crieit.now.sh/upload_images/7a5da27598b0ac7c7bde3fb86338c228661e8d0c5f2ec.webp" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7a5da27598b0ac7c7bde3fb86338c228661e8d0c5f2ec.webp?mw=700" alt="Untitled_result.webp" /></a></p> <p>greetを実行すると以下のように表示されるはずです。<br /> <a href="https://crieit.now.sh/upload_images/c069e96e53f499d91294f7f327e3c24f661e8d1ce2c95.webp" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c069e96e53f499d91294f7f327e3c24f661e8d1ce2c95.webp?mw=700" alt="Untitled2_result.webp" /></a></p> <p>はい、これにてviteでGASを書くための環境ができました。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>今回はローカルでGASを書くためviteでビルドする環境を作ってみました。<br /> viteは裏側がrollupであり、プラグインもrollupのものが使えるようなので、プラグインを見てから判断してもよいかもしれません。</p> <p><a target="_blank" rel="nofollow noopener" href="https://ja.vitejs.dev/plugins/">プラグイン | Vite https://ja.vitejs.dev/plugins/</a></p> <p>今回は、ここまで。</p> <p>おわり</p> しきゆら tag:crieit.net,2005:PublicArticle/18735 2024-02-09T02:11:50+09:00 2024-02-09T09:29:30+09:00 https://crieit.net/posts/JavaScript-TOTP JavaScript で TOTP を計算する <p>TOTP: Time-Based One-Time Password Algorithm とは、二要素認証でよく見る <a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=ja&gl=US">Google Authenticator</a> 等で QRコード を読み込ませたりして、一定時間毎に替わる 6桁 くらいの数字を入力するアレだ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://tex2e.github.io/rfc-translater/html/rfc6238.html">RFC 6238</a> に詳しい仕様が書かれている。<br /> コイツを TOTP をブラウザ上で生成したい。</p> <p>幸い近年の Web API には、バイナリ操作やクリプトまわりの機能が出揃っているので、わりかし簡単に実装できそうだ。<br /> …とまぁ、そんな愚生が思いつくような話など既に、偉大なる先人たちによる多くのサンプルが残されている。</p> <p>せっかく自分で実装するなら、最新の ES 仕様や Web API を活用して簡潔に、それでいて汎用的な仕様で書いてみよう。</p> <h2 id="実装"><a href="#%E5%AE%9F%E8%A3%85">実装</a></h2> <pre><code class="javascript">// @ts-check const base32Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; const base32AlphabetValuesMap = new Map([ ...Array.from(base32Alphabet, (key, index) => /** @type {[string, number]} */([key, index])), ]); /** @type { (strBase32: string) => Uint8Array } */ function decodeBase32(strBase32) { const strTrimmed = strBase32.replace(/=*$/, ""); const result = new Uint8Array(strTrimmed.length * 5 >>> 3); let dataBuffer = 0; let dataBufferBitLength = 0; let byteOffset = 0; for (const encoding of strTrimmed) { const value = base32AlphabetValuesMap.get(encoding); if (typeof value === "undefined") throw new Error("Invalid base32 string"); dataBuffer <<= 5; dataBuffer |= value; dataBufferBitLength += 5; if (dataBufferBitLength >= 8) { dataBufferBitLength -= 8; result[byteOffset++] = dataBuffer >>> dataBufferBitLength; } } if (dataBufferBitLength >= 5) throw new Error("Invalid base32 string"); if ((dataBuffer << (4 - dataBufferBitLength) & 0xf) !== 0) throw new Error("Invalid base32 string"); return result; } const hashAlgorithmNameMap = new Map([ ["SHA1", "SHA-1"], ["SHA256", "SHA-256"], ["SHA512", "SHA-512"], ]); /** @type { (secretUint8Array: Uint8Array, unixTimeMilliseconds: number, digits?: 6|7|8, period?: number, algorithm?: "SHA1"|"SHA256"|"SHA512") => Promise<string> } */ async function generateTOTP(secretUint8Array, unixTimeMilliseconds, digits = 6, period = 30, algorithm = "SHA1") { // unixTimeMilliseconds to step binary const stepsBuffer = new ArrayBuffer(8); const dv = new DataView(stepsBuffer); dv.setBigUint64(0, BigInt(Math.floor(unixTimeMilliseconds / 1000 / period))); // calc Hash const hashAlgorithmName = hashAlgorithmNameMap.get(algorithm); if (!hashAlgorithmName) throw new Error(`invalid algorithm: ${algorithm}`); const cryptKey = await crypto.subtle.importKey("raw", secretUint8Array, { name: "HMAC", hash: hashAlgorithmName }, false, ["sign"]); const hash = new Uint8Array(await crypto.subtle.sign("HMAC", cryptKey, stepsBuffer)); // Truncate const offset4Bit = hash.at(-1) & 0xf; const binary = ( (hash[offset4Bit] & 0x7f) << 24 | hash[offset4Bit + 1] << 16 | hash[offset4Bit + 2] << 8 | hash[offset4Bit + 3] ); // stringify const otp = binary % Math.pow(10, digits); return otp.toString().padStart(digits, "0"); } </code></pre> <p>実行例</p> <pre><code class="javascript">// 実行例 await generateTOTP(decodeBase32('XXXXXXXXXXXXXXXX'), Date.now(), 8, 30, "SHA1"); // 実行例2 => "94287082" await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8) // 実行例2.2 => "94287082" await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8, 30, "SHA1") // 実行例3 => "32247374" await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8, 30, "SHA256") // 実行例4 => "69342147" await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8, 30, "SHA512") </code></pre> <p>ハッシュアルゴリズムや、表示桁数、表示期間と言ったパラメータを変化させて、TOTP を出力できる。</p> <h2 id="応用例"><a href="#%E5%BF%9C%E7%94%A8%E4%BE%8B">応用例</a></h2> <p>例えば、前回の QRコード リーダーと組み合わせれば、 TOTP の管理を Web アプリ側で完結させるような実装もできる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/?p=2028">D&DしたQRコード画像をブラウザ上でデコードする | Aqua Ware つぶやきブログ</a></p> <p>ちゃんと多要素認証の一要素として所有者認証を満たせるよう、実装にはかなり気をつける必要はあるだろうが。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://tex2e.github.io/rfc-translater/html/rfc6238.html">RFC 6238</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Key Uri Format · google/google-authenticator Wiki</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kerupani129/items/4f3b44b2e00d32731ca4">[JavaScript] Unicode 文字列やバイナリデータを Base32 エンコードおよびデコードする #JavaScript - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kerupani129/items/4780fb1eea160c7a00bd">JavaScript で HOTP および TOTP を計算する #JavaScript - Qiita</a></li> </ul> advanceboy tag:crieit.net,2005:PublicArticle/18734 2024-02-08T01:37:05+09:00 2024-02-08T01:37:05+09:00 https://crieit.net/posts/D-D-QR D&DしたQRコード画像をブラウザ上でデコードする <p>ブラウザ上にドラッグ&ドロップ (D&D) されたQRコードの画像を、読み込んでデコードするUIを作りたい。</p> <p>Pure JavaScript で動作する、QR コードデコーダー(パーサー) <a target="_blank" rel="nofollow noopener" href="https://github.com/cozmo/jsQR">cozmo/jsQR</a> が有名なので、これを使ってみよう。</p> <p>jsQR は、 png や jpg といった画像コンテナの解凍機能も、 HTML や DOM, Web 周りに特化した機能も一切無いため、読み込ませた画像は何らかの方法で <code>Uint8ClampedArray</code> な RAW 画像に変換して渡す必要がある。</p> <p>ちょっと遠回りにはなるが、</p> <ol> <li>D&D されたファイルの <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/File_API">File API</a> を読み取る</li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/FileReader">FileReader API</a> を使って、ファイルを DataURI として読み込ませ、 img タグに表示させる</li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas">OffscreenCanvas</a> と <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvasRenderingContext2D">OffscreenCanvasRenderingContext2D</a> を経由して <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/en-US/docs/Web/API/ImageData">ImageData</a> を作成する</li> <li>jsQR に処理を投げる</li> </ol> <p>といった処理になるだろうか。</p> <p>早速実装してみよう。</p> <h2 id="実装"><a href="#%E5%AE%9F%E8%A3%85">実装</a></h2> <pre><code class="html"><div id="divDrop" style="margin: 10px; padding: 10px; background-color: lightgray; border: 4px dashed gray; border-radius: 12px;"> <div>Drag and drop an image file here</div> <div>or <input id="iptFile" type="file" accept="image/*"></div> <div id="divPreviewContainer"></div> </div> <div style="margin: 10px; padding: 10px;"> <input type="text" id="iptResult" style="width: 600px"> <div id="divErrorOut" style="display: none; margin: 4px; padding: 4px; background-color: pink; border: 1px solid red; border-radius: 4px;"></div> </div> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.min.js"></script> <script type="text/javascript"> // @ts-check (() => { "use strict"; const divDrop = /** @type {HTMLDivElement} */(document.getElementById("divDrop")); const divPreviewContainer = /** @type {HTMLDivElement} */(document.getElementById("divPreviewContainer")); const divErrorOut = /** @type {HTMLDivElement} */(document.getElementById("divErrorOut")); const iptFile = /** @type {HTMLInputElement} */(document.getElementById("iptFile")); const iptResult = /** @type {HTMLInputElement} */(document.getElementById("iptResult")); /** @type { (file: File) => Promise<any> } */ async function decodeQrCode(file) { divErrorOut.style.display = "none"; try { // read ile const fileReader = new FileReader(); const fileReadAsync = new Promise((resolve, reject) => { fileReader.onload = ev => resolve(ev.target?.result); fileReader.onerror = ev => reject(ev); }); fileReader.readAsDataURL(file); /** @type {string} */ const dataUrl = await fileReadAsync; // load as image divPreviewContainer.innerHTML = ""; const imgPreview = document.createElement("img"); const imgLoadAsync = new Promise(resolve => imgPreview.onload = ev => resolve(ev)) imgPreview.setAttribute("src", dataUrl); divPreviewContainer.append(imgPreview); await imgLoadAsync; const { naturalWidth, naturalHeight } = imgPreview; // convert image to raw binary var canvas = new OffscreenCanvas(naturalWidth, naturalHeight); var ctx = canvas.getContext("2d"); if (!ctx) throw "failure to getContext"; ctx.drawImage(imgPreview, 0, 0); const imageData = ctx.getImageData(0, 0, naturalWidth, naturalHeight); // decode with jsQR const code = jsQR(imageData.data, naturalWidth, naturalHeight); if (code) { iptResult.value = code.data; } else { iptResult.value = ""; throw "decode QR error"; } } catch (err) { divErrorOut.style.display = "block"; divErrorOut.textContent = `${err}`; } } // Handling D&D divDrop.addEventListener("dragover", ev => { ev.preventDefault(); }); divDrop.addEventListener("drop", ev => { ev.preventDefault(); let imageFiles; if (ev.dataTransfer && 0 < (imageFiles = [...ev.dataTransfer.files].filter(f => f.type.startsWith("image/"))).length) { const dt = new DataTransfer(); imageFiles.forEach(f => dt.items.add(f)); iptFile.files = dt.files; decodeQrCode(imageFiles[0]); } }); iptFile.addEventListener("change", ev => iptFile.files && 0 < iptFile.files.length ? decodeQrCode(iptFile.files[0]) : undefined) })(); </script> </code></pre> <p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="GReBXOM" data-user="advanceboy" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a target="_blank" rel="nofollow noopener" href="https://codepen.io/advanceboy/pen/GReBXOM"> decode D&Ded QR images</a> by advanceboy (<a target="_blank" rel="nofollow noopener" href="https://codepen.io/advanceboy">@advanceboy</a>) on <a target="_blank" rel="nofollow noopener" href="https://codepen.io">CodePen</a>.</span> </p> <p>いったん img タグで画像を表示させるワンクッションを置いているおかげで、 png, jpg のみならず svg 等の画像もデコードできるようになっている。</p> <p>Edge や Chrome ブラウザであれば、表示されている画像をそのまま D&D してきてのデコードもできるぞ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2024/02/decode-drag-and-droped-qr-image-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2024/02/decode-drag-and-droped-qr-image-01-1024x564.png" alt="" /></a></p> <p>jsQR の処理は、 JavaScript のスレッドで行われるので、処理が重くなるとブラウザが固まってしまう。<br /> このため、理想的には Web Worker に処理を渡してしまうほうがよさそうではある。</p> <p>ただ、1000x1000ピクセル程度の画像であれば、近年のデバイスなら一瞬でデコードできるので、実際のところはメインスレッド側で処理してしまって問題ないだろう。</p> advanceboy tag:crieit.net,2005:PublicArticle/18733 2024-02-08T01:34:00+09:00 2024-02-08T01:35:45+09:00 https://crieit.net/posts/URL-65c3b0f86a9e2 ブラウザのタイトルや URL をコピーするブックマークレット【スマホ&阿部寛対応】 <p>ブラウザで表示しているページのタイトルと URL を一括でコピーしたり、 markdown などのマークアップ言語でフォーマットされたものを、クリップボードにコピーしたくなることは無いだろうか?</p> <p>意外にも、主要なブラウザには表示ページのタイトル名すら簡単にコピーする方法がない。</p> <p>いくつかのブラウザ拡張機能(アドオン)には、上述の機能を提供しているものがいくつもある。<br /> しかし、これだけのために拡張機能を入れるのもなんだかなと。<br /> 場合によっては、ポリシー等様々な理由で拡張機能を入れることを制限されている場合もあるだろうし。</p> <p>そこで、表示中ページのタイトルと URL を任意の種類にフォーマットしてクリップボードへコピーしてくれるブックマークレットを作ってみた。</p> <p>ブックマークレットとした事により、拡張機能の使えないスマートフォンの Chrome や Safari といった、とても不便なブラウザでも利用できる。</p> <h2 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h2> <p><a target="_blank" rel="nofollow noopener" href='javascript:(()=>{let e="FRAMESET"===top.document.body.tagName?top.frames[0].document:document,t=t=>e.getElementById(t),i=e.createElement("div"),l="modalBMId_key";i.innerHTML=%60<div id=${l}m style="position:fixed;width:100%;height:100%;top:0;text-align:center;background:rgba(0,0,0,.5);padding:16px 20px;z-index:999"><div style="display:inline-block;background:#fff"><div style="padding:8px"><div id=${l}b></div><div><label for=${l}i style="color:#000">copied:</label><input id=${l}i style="width:400px" readonly/></div></div></div></div>%60;let r=t(l+"m");r||(e.body.prepend(i.firstChild),Object.entries({TitleOnly:"<span>{</span><span>{</span>title<span>}</span><span>}</span>",Text:"<span>{</span><span>{</span>title<span>}</span><span>}</span>\n<span>{</span><span>{</span>url<span>}</span><span>}</span>",Markdown:String.raw%60[<span>{</span><span>{</span>title/\\/\\/(?=[\[\]])/\<span>}</span><span>}</span>](<span>{</span><span>{</span>url/\)/%2529<span>}</span><span>}</span>)%60,HTML:String.raw%60<a href="<span>{</span><span>{</span>url/&/&amp;/"/&quot;<span>}</span><span>}</span>"><span>{</span><span>{</span>title/</&lt;<span>}</span><span>}</span></a>%60,Textile:String.raw%60"<span>{</span><span>{</span>title/&/&amp;/"/&quot;/\]/&#93;<span>}</span><span>}</span>":<span>{</span><span>{</span>url/\]/%255D<span>}</span><span>}</span>%60,AsciiDoc:String.raw%60link:++<span>{</span><span>{</span>url<span>}</span><span>}</span>++[<span>{</span><span>{</span>title/\[/&#91;/\]/&#93;<span>}</span><span>}</span>]%60,Jira:String.raw%60[<span>{</span><span>{</span>title/&/&amp;/\[/&#91;/\]/&#93;/\|/&#124;<span>}</span><span>}</span>|<span>{</span><span>{</span>url/\[/%255B/\]/%255D/\|/%257C<span>}</span><span>}</span>]%60,LaTeX:String.raw%60\href<span>{</span><span>{</span>{url/\\/\backslash/(?=[&%$#_{}])/\<span>}</span><span>}</span>}<span>{</span><span>{</span>{title/\\/\backslash/(?=[&%$#_{}])/\<span>}</span><span>}</span>}%60}).map(([i,r])=>{let a=e.createElement("button");a.style.color="#000",a.textContent="copy - "+i,a.onclick=()=>{let i=r.replace(/<span>{</span><span>{</span>((title|url)(?:\/(.*?)\/(.*?))?(?:\/(.*?)\/(.*?))?(?:\/(.*?)\/(.*?))?(?:\/(.*?)\/(.*?))?)<span>}</span><span>}</span>/ig,function(t,i,l){let r="title"==l.toLowerCase()?e.title:location.href;for(let a=3;a<10;a+=2){let d=arguments[a];if(d&&"string"==typeof d)r=r.replace(RegExp(d,"g"),arguments[a+1]??"");else break}return r});navigator.clipboard.writeText(i),t(l+"i").value=i},t(l+"b").append(a)}),r=t(l+"m"),e.addEventListener("click",e=>{e.target==r&&r.remove()}))})();'>ブラウザのタイトルや URL をコピーするブックマークレット </a></p> <p>上記のリンクを(Firefox なら右クリックから直接、 Edge や Chrome なら ブックマークバーに D&D する等の方法で)ブックマークとして保存し、任意のページで呼び出すと、下記のようなダイアログが表示される。<br /> お好きなボタンをクリックすれば、クリップボードにフォーマット済みのリンクが格納されるという算段だ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2024/01/copy-formatted-link-bookmarklet-01.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2024/01/copy-formatted-link-bookmarklet-01-1024x82.png" alt="" /></a></p> <p>暗転したところをクリックすれば、ダイアログはクローズされる。</p> <p>最低限のコードにするため、 CSS は表示中のページのものを使っており、開いたページによってダイアログのデザインが変わるのは仕様となっている。</p> <p>Firefox, Chrome, Edge いずれでも動作することは確認している。<br /> Edge の場合ツールバーの「お気に入り☆」ボタンからではブックマークレットが動かないようなので、ブックマークバーから実行する必要があるようだ。(v121 現在)</p> <p>もちろん、「阿部 寛のホームページ」にも対応している。<br /> このようにフレームセットを使っている場合は、最初のフレームにダイアログを表示させる形で、対応している。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2024/01/copy-formatted-link-bookmarklet-02.png"><img src="https://aquasoftware.net/blog/wp-content/uploads/2024/01/copy-formatted-link-bookmarklet-02-1024x180.png" alt="" /></a></p> <h2 id="カスタマイズ方法"><a href="#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA%E6%96%B9%E6%B3%95">カスタマイズ方法</a></h2> <p>既定で markdown, textile, LaTeX などいくつかのテンプレートを用意している。</p> <p>もし追加したいものがあれば、後述のソースコードの <code>Object.entries({</code>の後ろに、 JSON キーにボタンのタイトルを、 JSON 値に変換のテンプレートを指定したものを追加し、 各自 minify して使ってもらえれば OK だ。(ライセンスは CC-0)</p> <p>ソースコード中に <code>%5B</code>, <code>%5D</code> などといった URL エンコードと解釈できてしまう部分があるため、ブックマークの編集画面の URL に直接貼り付ける場合は、 <code>%255B</code>, <code>%255D</code> などとエスケープしておこう。</p> <p>なお、テンプレートの定義方法だが、 <code><span>{</span><span>{</span><span>}</span><span>}</span></code> で括った部分に <code>title</code> (ページタイトル) もしくは <code>url</code> と、 0~8 の偶数個の <code>/</code> を記述し、置換対象を表す正規表現と、置換後の文字列を交互に記述する。</p> <p>例えば、</p> <pre><code class="js">String.raw`<span>{</span><span>{</span>url/\[/%5B/\]/%5D/\|/%7C<span>}</span><span>}</span>` </code></pre> <p>なら、以下と等価となる。</p> <pre><code class="js">url.replace(RegExp(String.raw`\[`, "g"), "%5B") .replace(RegExp(String.raw`\]`, "g"), "%5D") .replace(RegExp(String.raw`\|`, "g"), "%7C"); </code></pre> <p>雑な説明だがわかって。</p> <gist src="https://gist.github.com/advanceboy/a960ec38442165a7df93408d775b03eb.js"></gist> advanceboy tag:crieit.net,2005:PublicArticle/18302 2022-10-03T20:23:11+09:00 2022-10-03T20:23:11+09:00 https://crieit.net/posts/mac-downshift macでdownshiftを使うと変換時の確定がおかしい <p>macでdownshiftを使うと、変換中にEnterキーで入力を確定するとその時点でdownshiftに確定が伝播されてしまい、タグの入力が確定してしまう。</p> <p>原因としてはcompositionendイベントがうまく発火していない、もしくは内部で正しく処理できていないため。</p> <p>そのためdownshiftのonKeyDownイベントにて、isComposingをチェックして変換中かどうかをチェックして処理を行う必要がある。</p> <pre><code class="typescript"> onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter' && !e.nativeEvent.isComposing) { e.preventDefault() setText({ name: inputText }) } }, </code></pre> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/18287 2022-08-24T23:45:46+09:00 2022-08-24T23:53:31+09:00 https://crieit.net/posts/clean-dist-folder-by-webpack 【JS/Webpack】ビルドしたフォルダの不要ファイルを削除する <p>こんにちは、しきゆらです。今回は、Webpackでビルドしたものが出力されるフォルダにできる不要ファイルを削除する方法を知ったのでメモしておきます。</p> <p>WebpackでJSのコードをビルドしていると、結構な頻度で不要なファイルができたりします。例えば、デバッグ用に生成したソースマップファイルがそのまま残っていたり、過去に設定を変えてあるところから出力しなくなったファイルが残っていたり等、ビルド先のdistフォルダの中はごちゃごちゃしがちです。これを何とかしたいなぁ、と思って調べたら簡単に解決できました。</p> <p>Webpackでこういうことできないかなぁと調べると大抵プラグインが出てきて追加しないといけないことが多く、今回もそのパターンでよくわからないものを追加しないといけないのかなぁ・・・と思っていたのですが、どうも標準機能として用意されているようでした。ということで結論。</p> <h2 id="結論: outputの中に[clean: true]を追加すればよい"><a href="#%E7%B5%90%E8%AB%96%3A+output%E3%81%AE%E4%B8%AD%E3%81%AB%5Bclean%3A+true%5D%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8C%E3%81%B0%E3%82%88%E3%81%84">結論: outputの中に[clean: true]を追加すればよい</a></h2> <p>ドキュメントを読むことは大事ですね。しっかり記載がありました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder">https://webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder</a></p> <p>こちらによると、ビルドしたものを置くdistフォルダの中身をビルドのたびにきれいにしたい場合はoutput.cleanオプションを使え、とあります。ただし、この方法が使えるのはWebpack5以降のみなので注意してください。</p> <pre><code class="javascript">// webpack.config.jsの一部分 ~~~~~~ output: { filename: "output.js", path: path.resolve(__dirname, "dist"), clean: true // この1行を追加すればよい } </code></pre> <p>はい、たったこれだけ。1行追加するだけで、ビルドのたびにdistフォルダの中身をきれいにしてくれるので、過去にビルドした残骸が残ることはなくなりました。</p> <p>また、こちらのページには細かい設定方法もありました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://v4.webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder">https://v4.webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder</a></p> <p>そして、前述の通りこの方法が使えるのはWebpack5以降のみとのことでした。Webpack4などを使っている方は、プラグインでの対応となるようです。</p> <h2 id="Webpack4を使っている方: clean-webpack-pluginを使う"><a href="#Webpack4%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E6%96%B9%3A+clean-webpack-plugin%E3%82%92%E4%BD%BF%E3%81%86">Webpack4を使っている方: clean-webpack-pluginを使う</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://v4.webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder">https://v4.webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder</a></p> <p>Webpack4版のドキュメントを見ると、同様の項目ですが対応方法が異なっていました。「clean-webpack-plugin」というプラグインを使えば削除できるので、導入・設定しましょうとのことでした。以下にドキュメントにあったサンプルを載せておきます。</p> <pre><code class="bash"># プラグインのインストール npm install --save-dev clean-webpack-plugin </code></pre> <p>webpack.config.jsはこんな感じ。</p> <pre><code class="javascript">// webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); // プラグインを読み込む const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { entry: { app: './src/index.js', print: './src/print.js', }, plugins: [ new CleanWebpackPlugin(), // この行を追加する new HtmlWebpackPlugin({ title: 'Output Management', }), ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, }; </code></pre> <p>これにより、output.pathで指定したフォルダにある不要ファイルを削除したうえでビルドしてくれるとのことでした。さらに、Webpack5では細かい設定ができましたが、clean-webpack-pluginでも似たような設定はできそうです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/clean-webpack-plugin">https://www.npmjs.com/package/clean-webpack-plugin</a></p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>今回は、Webpackで過去にビルドした残骸をきれいに削除する方法をまとめました。不必要なプラグインを導入せずに、たった1行の設定追加だけでやりたかったことが実現できてとてもハッピーです。</p> <p>そして、Webpack5ではデフォルトでこの機能が入っていることを知りませんでした。過去に調べたときはプラグイン対応だったよなぁと思いつつ調べると、やはりプラグインを導入しろ、という記事が多く出てきました。とりあえずググるのは大事ですが、ドキュメントをさっくりと眺めておく、というのも必要ですね。</p> <p></p> しきゆら tag:crieit.net,2005:PublicArticle/18281 2022-08-14T18:34:08+09:00 2022-08-14T20:57:55+09:00 https://crieit.net/posts/js-filtermap-can-remove-element 【JS】flatMapで不要なものを削除したい <p>こんにちは、しきゆらです。今回は、flatMapの処理の中で不要な要素が出てきた場合にそれを排除する方法を知ったのでメモしておきます。</p> <p><a target="_blank" rel="nofollow noopener" rel="noreferrer noopener" href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap" target="_blank">https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap</a>こちらのページの「<a target="_blank" rel="nofollow noopener" rel="noreferrer noopener" href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap" target="_blank">map()のアイテムの追加と削除</a>」の項目にある通り、処理の中で空配列([])を返すと、その要素の処理を削除することができるようです。</p> <pre><code class="javascript">let array = [1,2,3,4,5,6,7,8]; array.flatMap( (item) => { if (item % 2 === 0) { return item * 2; } else { return []; } }) // => [4, 8, 12, 16] </code></pre> <p>なお、同様の処理をmapで行うと、そのまま要素が空配列に置き換わってしまいました。(それはそう)</p> <pre><code class="javascript">let array = [1,2,3,4,5,6,7,8]; array.map( (item) => { if (item % 2 === 0) { return item * 2; } else { return []; } }) // =>[Array(0), 4, Array(0), 8, Array(0), 12, Array(0), 16] </code></pre> <p>ということで、flatMapの場合は処理の中で不要な要素を削除ができるようです。</p> <p>なぜ、flatMapで空配列を返すと要素を削除することができるのかというのは</p> <blockquote> <p>flatMap()はmap()の後にflat()を行うのと同じ<br /> 参照元: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap</p> </blockquote> <p>とある通り、処理の中でflat()をかけるので空配列は消えてしまいます。ということで、flatMapの中で空配列を返してあげれば要素を削除することができるよ、という話でした。</p> <p>・・・で、これで終わるとしょうもないので、Rubyで同様のことができるのかやってみました。<a target="_blank" rel="nofollow noopener" href="https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/collect_concat.html" target="_blank" rel="noreferrer noopener">https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/collect_concat.html</a></p> <pre><code class="ruby">irb(main):001:0> a = [1,2,3] => [1, 2, 3] irb(main):002:0> b = [-1, 0, 9] => [-1, 0, 9] irb(main):003:0> list = [a, b] => [[1, 2, 3], [-1, 0, 9]] irb(main):004:0> list.flat_map{|array| array.select{|i| i < 0<span>}</span><span>}</span> => [-1] irb(main):005:0> list.flat_map{|array| array.include?(-1)? []: array} => [1, 2, 3] </code></pre> <p>こちらも同様で、空配列を返すと削除される動きをしています。ただ、リファレンスにはそのような記載はないので、当たり前だよね、として書かれていないのかなと。</p> <h2 id="不要な要素をはじく場合、flatMapとmap+αはどっちが速いのか"><a href="#%E4%B8%8D%E8%A6%81%E3%81%AA%E8%A6%81%E7%B4%A0%E3%82%92%E3%81%AF%E3%81%98%E3%81%8F%E5%A0%B4%E5%90%88%E3%80%81flatMap%E3%81%A8map%2B%CE%B1%E3%81%AF%E3%81%A9%E3%81%A3%E3%81%A1%E3%81%8C%E9%80%9F%E3%81%84%E3%81%AE%E3%81%8B">不要な要素をはじく場合、flatMapとmap+αはどっちが速いのか</a></h2> <p>この手の処理はmap()メソッドの前後で特定の値をはじいたりしていましたが、どちらが速いのでしょう。気になったので、ついでに調べてみました。ここでもJSとRubyで測ってみました。</p> <p>例としては微妙ですが、-5から5の間の乱数の中で0以上の場合に3倍する処理をflatMapと他の処理で処理時間を計測してみます。</p> <h3 id="JSの場合"><a href="#JS%E3%81%AE%E5%A0%B4%E5%90%88">JSの場合</a></h3> <p>こんな感じの雑さで試してみます。</p> <p>なお、処理の中で不要なコードが混じってますが一応2つの処理で同じ結果が返ってくるかを確認したかったのでequalArrayという雑チェック関数を作ってます。</p> <pre><code class="javascript">import * as Benchmark from "benchmark"; const filterMapTest = (array) => { return array.filter((item) => { return item >= 0; }).map(item => item*3) } const flatMapTest = (array) => { return array.flatMap((item) => { if(item >= 0) { return item * 3; } else { return []; } }) } const equalArray = (a, b) => { if(!Array.isArray(a)) return false; if(!Array.isArray(b)) return false; if(a.length !== b.length) return false; for(let i = 0; i < a.length; i += 1) { if(a[i] !== b[i]) return false; } return true; } // initialize const minNum = -5; const maxNum = 5; const array = [...Array(99*99)].map(_ =>; Math.floor(Math.random() * 10) - 5); let suite = new Benchmark.Suite // benchmark suite .add("filter + map", () => { filterMapTest(array) }) .add("flatMap", () => { flatMapTest(array) }) .on("cycle", (event) => { console.log(String(event.target)) }) .on("complete", function() { console.log("Fastest is " + this.filter("fastest").map("name")); }) .run({async: true}) const filterTest = filterMapTest(array); const flatTest = flatMapTest(array); console.log(equalArray(filterTest, flatTest)) </code></pre> <p>実行結果はこんな感じでした。</p> <pre><code class="zsh">$ > node dist/bench.js true filter + map x 21,538 ops/sec ±18.58% (87 runs sampled) flatMap x 1,673 ops/sec ±0.93% (96 runs sampled) Fastest is filter + map </code></pre> <p>大きな差はなさそうですが、flatMapよりもfilter+mapのほうが速そうです。</p> <h3 id="Rubyの場合"><a href="#Ruby%E3%81%AE%E5%A0%B4%E5%90%88">Rubyの場合</a></h3> <p>こんな感じの雑さで試してみました。</p> <pre><code class="ruby">require "benchmark" def map_uniq(array) array.map { |item| item.negative? ? next : item * 3}.compact end def flatmap(array) array.flat_map{|item| item.negative? ? []: item * 3 } end array = Array.new(99*99){ rand(-5...5) } Benchmark.bmbm do |r| r.report("flatMap") { flatmap(array) } r.report("map_uniq") { map_uniq(array) } end </code></pre> <p>実行結果はこんな感じでした。</p> <pre><code class="zsh">Rehearsal -------------------------------------------- flatMap 0.001012 0.000000 0.001012 ( 0.001012) map_uniq 0.000562 0.000000 0.000562 ( 0.000562) -----------------------------------total: 0.001574sec user system total real flatMap 0.000815 0.000000 0.000815 ( 0.000814) map_uniq 0.000564 0.000000 0.000564 ( 0.000563) </code></pre> <p>よく考えなくても、flatMapのほうは無駄に配列生成して処理スキップさせているので遅いよなぁ、という印象でした。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>今回は、JSのflatMapで不要な要素をはじいて処理することができる、ということを知ったのでメモしました。また、気になったので、同様な処理がRubyでもできるか確認したり、flatMapで不要要素を弾く場合とfilterなどをかけた場合の処理時間の差を見てみました。</p> <p>今回はここまで。おわり</p> しきゆら tag:crieit.net,2005:PublicArticle/18243 2022-07-17T07:52:23+09:00 2022-07-17T07:52:23+09:00 https://crieit.net/posts/JavaScript-Cookie JavaScript: Cookieから特定要素を見つけ出す際のトリム <p>jQuery.Cookieなどのラッピングされたものを使用する場合には、なんの考慮もいらないはずです。<br /> 裸のJavaScriptでハンドリングする場合に忘れがちなのがトリムです。</p> <h2 id="サンプル"><a href="#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB">サンプル</a></h2> <pre><code>function getCookie (key) { var cookies = document.cookie; var cookiesArray = cookies.split(';'); for (var tuple of cookiesArray){ var strArray = tuple.split('='); var str = strArray[0]; if (str.trim() == key) { // トリムしてあげましょう str = strArray[1]; return str.trim(); } } return ''; } </code></pre> <p>CやPerlなんかで書く場合には、まあ忘れることはないんですけどね。私の場合、JavaScriptの場合が特に油断しますね。<br /> トリムのオーバーヘッドなんてわずかでしょうからね、私の場合は、要らないんじゃない?と思える場合でもとりあえずトリムしますよ。</p> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/18242 2022-07-16T15:21:02+09:00 2022-07-16T15:21:02+09:00 https://crieit.net/posts/HTML-JavaScript HTML + JavaScript: 見出しやボタンなどの表示文字列を統一させたいとき <p>とあるシステムに登場する複数のWEB画面のあいだで、見出しやボタンの表示文字列を統一させたい場合に、私が使う手法のひとつがこれです。<br /> いろんな方が、これと同じような手法をとられていると思います。かな?</p> <h2 id="サンプル"><a href="#%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB">サンプル</a></h2> <p>JavaScript側からです。これを各HTMLにインクルードして使います。</p> <pre><code>// JavaScript function label_print () { var arrVar = { label_lastname: "姓", label_firstname: "名", label_birthday: "生年月日", }; var arrVarButton = { button_save: "保存", button_cancel: "キャンセル", }; for (let key in arrVar) { if(document.getElementById(key) != null) { $('#' + key).html(arrVar[key]); } } for (let key in arrVarButton) { if(document.getElementById(key) != null) { $('#' + key).val(arrVarButton[key]); } } } </code></pre> <p>HTML側です。</p> <pre><code><!-- HTML --> ... <body class="" onload="label_print();"> ... <td class="label" style="white-space: nowrap;"> <div id="label_lastname"></div> </td> ... </code></pre> <p>サーバ・サイドで、データベースなどを用いて処理する手法もあったり、いろんな手法があっていずれも賛否両論だと思います。上記はレガシーで単純、必要なパーツも少なく取り回しが楽ですね。Simple is best.</p> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/18239 2022-07-14T23:39:50+09:00 2022-07-14T23:39:50+09:00 https://crieit.net/posts/extract-array-of-diff-exclude-duplicated-between-two-arraies-by-javascript-20220715 JavaScript で互いにオブジェクトを要素に持つ配列2つを比較して、重複していない要素のみを取り出す <h2 id="経緯"><a href="#%E7%B5%8C%E7%B7%AF">経緯</a></h2> <p>JavaScript で、互いにオブジェクトを要素に持つ配列2つを比較して、重複していない要素のみを取り出す処理を試みたのでメモしておきます。</p> <h2 id="前提"><a href="#%E5%89%8D%E6%8F%90">前提</a></h2> <p>前提として、次のような2つの配列があったとします。</p> <ul> <li>互いにオブジェクトを要素として持つ配列</li> <li>配列2は配列1の子集合(サブセット)</li> </ul> <p>この2つの配列を比較して、配列1の中から<strong>配列2に含まれない要素のみ</strong>の配列を作りたい、と考えました。</p> <p>想定する最終結果も併せて付記しておきます。</p> <h3 id="配列1"><a href="#%E9%85%8D%E5%88%971">配列1</a></h3> <pre><code class="json">[ { "value": "value-0", "label": "Château d'If" }, { "value": "value-1", "label": "Marseille" }, { "value": "value-2", "label": "France" }, { "value": "value-3", "label": "Le Comte de Monte-Cristo" }, { "value": "value-4", "label": "Alexandre Dumas" }, { "value": "value-5", "label": "Rhino" }, { "value": "value-6", "label": "prison" }, { "value": "value-7", "label": "François I" }, { "value": "value-8", "label": "Jean-Baptiste Kléber" } ] </code></pre> <h3 id="配列2"><a href="#%E9%85%8D%E5%88%972">配列2</a></h3> <pre><code class="json">[ { "value": "value-0", "label": "Château d'If" }, { "value": "value-1", "label": "Le Comte de Monte-Cristo" }, { "value": "value-2", "label": "Alexandre Dumas" } ] </code></pre> <h3 id="得たい配列"><a href="#%E5%BE%97%E3%81%9F%E3%81%84%E9%85%8D%E5%88%97">得たい配列</a></h3> <pre><code class="json">[ { "value": "value-1", "label": "Marseille" }, { "value": "value-2", "label": "France" }, { "value": "value-5", "label": "Rhino" }, { "value": "value-6", "label": "prison" }, { "value": "value-7", "label": "François I" }, { "value": "value-8", "label": "Jean-Baptiste Kléber" } ] </code></pre> <p>「2つの配列で重複した要素を除去した配列を生成する」というのはいくつか記事を見かけました。</p> <p>しかし、それらは1次元配列のサンプルばかりだった上に、今回はさらに「要素の中の <code>label</code>キー の値で比較したい」という内容だったので、さらにハードルが上がりました。</p> <h2 id="コード"><a href="#%E3%82%B3%E3%83%BC%E3%83%89">コード</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.nxworld.net/js-array-filter-snippets.html">JavaScript:filter()を使って配列内の重複要素を削除・取得したり、2つの配列から共通要素を取得する方法 - NxWorld</a></li> </ul> <p>最終的にはこちらのコードをベースにして作りました。</p> <p>サンプルは以下。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://arm-band.github.io/test-l-enfer-de-chateau-d-if/">Home - L'enfer de Chateau d'If</a></li> </ul> <pre><code class="javascript">// 配列 const Ar1 = [ // 略 ]; const Ar2 = [ // 略 ]; // 重複要素を除去した配列を返す関数 const getArraysDiff = (array1, array2) => { // 引数の各々の配列から label のみの配列を生成 const array1LabelArray = array1.map((itm) => { return itm.label; }); const array2LabelArray = array2.map((itm) => { return itm.label; }); // label のみの配列で比較 const arr1 = [...new Set(array1LabelArray)]; const arr2 = [...new Set(array2LabelArray)]; return [...arr1, ...arr2].filter((val) => { return !arr1.includes(val) || !arr2.includes(val); }); }; // 上述関数で重複除去した label の配列を得る const ChateuDiff = getArraysDiff(Ar1, Ar2); // filter メソッドで、元配列 から該当する label が存在する要素を除去する const enferChateuDiff = Ar1.filter((item) => { return ChateuDiff.includes(item.label); }); </code></pre> <p>ざっくりこのような処理で想定していた結果を得られました。</p> <h2 id="余談"><a href="#%E4%BD%99%E8%AB%87">余談</a></h2> <p>何故このようなことをしようかと思ったかというと、 <a target="_blank" rel="nofollow noopener" href="https://react-select.com/home">React Select</a> で <code>defaultCalue</code> で指定した選択済みの項目が選択候補にも上がってしまっていたので、除外しようとしたためでした。</p> <p>が、そもそも React Select 側は選択済み項目を除外する機能を元々持っていたので上述の処理は不要ということが分かったため、今回のコードは未使用となりました。</p> <p>ちなみに、この機能が働かなかった原因は API で取得した 値から 上述のような <code>label</code> と <code>value</code> のオブジェクトを生成するループ処理の際に、 <code>value</code> に ID の数値を振り方を間違えていたため、 React Select から「異なる値」として認識されてしまっていたためでした。</p> <p>また、仮に今回の処理をかけたとしても、初期表示では上手く選択済み項目が除外されますが、選択済み項目を削除した場合は選択可能な項目として再度選択肢に復活させる必要があるため、かなり手間がかかることが想定されたためオミットしていたと思います。</p> <p>……本当、 React Select 側に標準搭載されていて良かったです。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <h3 id="本題"><a href="#%E6%9C%AC%E9%A1%8C">本題</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.nxworld.net/js-array-filter-snippets.html">JavaScript:filter()を使って配列内の重複要素を削除・取得したり、2つの配列から共通要素を取得する方法 - NxWorld</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.dkrk-blog.net/javascript/duplicate_an_array">配列同士で重複する値があるか確認する | grgr-dkrkのブログ</a></li> </ul> <h3 id="label のみの配列を作る"><a href="#label+%E3%81%AE%E3%81%BF%E3%81%AE%E9%85%8D%E5%88%97%E3%82%92%E4%BD%9C%E3%82%8B">label のみの配列を作る</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tsu_eng/items/57f9d3919bf175188033">JavaScriptで連想配列から特定のキーだけ抽出 - Qiita</a></li> </ul> <h3 id="配列操作"><a href="#%E9%85%8D%E5%88%97%E6%93%8D%E4%BD%9C">配列操作</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/takeharu/items/d75f96f81ff83680013f">JavaScriptの配列の使い方まとめ。要素の追加,結合,取得,削除。 - Qiita</a></li> </ul> <h3 id="JSON のフォーマット"><a href="#JSON+%E3%81%AE%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88">JSON のフォーマット</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/unsoluble_sugar/items/7df08527215ea92831a6">JSON.stringifyの出力結果を整形して可読性を向上させる - Qiita</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/18181 2022-04-30T00:03:10+09:00 2022-04-30T00:03:10+09:00 https://crieit.net/posts/omit-jquery-note-20220502 脱jQuery メモ <h2 id="経緯"><a href="#%E7%B5%8C%E7%B7%AF">経緯</a></h2> <p>Bootstrap でブレークポイント未満(スマホ時)のナビゲーションリンクにアンカーリンクがある場合、アンカーリンクをタップしてもナビゲーションリンクのメニューが開いたままアンカーリンクへ遷移するので、それを制御するコードを自前で書いていました。その部分を脱 jQuery したのでメモしておきます。</p> <h2 id="コード"><a href="#%E3%82%B3%E3%83%BC%E3%83%89">コード</a></h2> <h3 id="jQuery"><a href="#jQuery">jQuery</a></h3> <p>まずは Bootstrap 4 までの jQuery でのコード。</p> <pre><code class="js">// ナビゲーションバー const $navbar = $('#navbar'); // ブランド名とドロップダウンコンポーネント以外のナビゲーションリスト $navbar.find('.navbar-brand, .nav-item:not(.dropdown) a, .dropdown-item').on('click', function (e) { // 略 const navBarListID = 'navbarList'; if ($(e.currentTarget).closest('#' + navBarListID).length > 0) { let breakpoint = 0; if (/(^|\s)navbar-expand-(\S*)/g.test($navbar.children('.navbar').prop('class'))) { switch (RegExp.$2) { case 'sm': breakpoint = 576; break; case 'md': breakpoint = 768; break; case 'lg': breakpoint = 992; break; case 'xl': breakpoint = 1200; break; default: breakpoint = 0; break; } } if ($(window).outerWidth() < breakpoint && !$navbar.find('.navbar-toggler[data-target="#' + navBarListID + '"]').hasClass('collapsed')) { // 現在の表示がハンバーガーメニューの場合 $navbar.find('.navbar-toggler[data-target="#' + navBarListID + '"]').trigger('click'); // 移動したらハンバーガーを折りたたむ } else if ($(e.currentTarget).hasClass('dropdown-item') && $(e.currentTarget).closest('.dropdown').hasClass('show')) { // 現在の表示がハンバーガーメニューではなく、ドロップダウン内のメニューをクリックした場合 $(e.currentTarget).closest('.dropdown').trigger('click'); // 移動したらドロップダウンを折りたたむ } } return false; } }; </code></pre> <p>ざっくりこんなコードでした。やっていることとしては</p> <ul> <li>ブランド(<code>.navbar-brand</code>), ドロップダウンではないナビゲーションリンク(<code>.nav-item:not(.dropdown) a</code>, ドロップダウン内のリンク(<code>.dropdown-item</code>) がクリックされた場合 <ul> <li>クリック(タップ)された要素の直近の祖先要素に <code>#navbarList</code> の id属性 がある要素が存在する場合 <ul> <li>ナビゲーションバー要素のクラスにある <code>.navbar-expand-XX</code> のクラスのブレークポイントの文字列を取得してブレークポイント値をセット</li> <li>その値と現在のウィンドウの幅を比較してウインドウ幅の方が小さい (=ハンバーガーメニューに表示が切り替わっている) 、かつ <code>.navbar-toggler</code> 要素が <code>.collapsed</code> のclass属性 を持っている (=メニュー展開) 場合 <ul> <li>ハンバーガーメニューを折りたたむ (ハンバーガーアイコンを1度クリックする)</li> </ul></li> <li>またはクリックされた要素が <code>.dropdown-item</code> class属性 を持っている (=ドロップダウンメニュー) 、かつ直近の祖先要素で <code>.dropdown</code> class属性 を持つ要素が <code>.show</code> class属性 を持っている (=ドロップダウンが開かれている) 場合 <ul> <li>ドロップダウン要素を折りたたむ</li> </ul></li> </ul></li> </ul></li> </ul> <p>という挙動。</p> <h3 id="プレーン JavaScript"><a href="#%E3%83%97%E3%83%AC%E3%83%BC%E3%83%B3+JavaScript">プレーン JavaScript</a></h3> <p>これを Bootstrap 5 対応でプレーンな JavaScirpt に書き換え。</p> <pre><code class="js">const navbar = document.querySelector('#navbar'); const navBarListID = 'navbarList'; if ( typeof e.currentTarget.closest(`#${navBarListID}`) !== 'undefined' && e.currentTarget.closest(`#${navBarListID}`) !== null ) { let breakpoint = 0; if (/(^|\s)navbar-expand-(\S*)/g.test(navbar.className)) { switch (RegExp.$2) { case 'sm': breakpoint = 576; break; case 'md': breakpoint = 768; break; case 'lg': breakpoint = 992; break; case 'xl': breakpoint = 1200; break; case 'xxl': breakpoint = 1400; break; default: breakpoint = 0; break; } } if (window.innerWidth < breakpoint) { // ブレークポイント未満の幅のとき const navbarTogglers = navbar.querySelectorAll(`.navbar-toggler[data-bs-target="#${navBarListID}"]`); navbarTogglers.forEach((navbarToggler) => { if(!navbarToggler.classList.contains('collapsed')) { // 現在の表示がハンバーガーメニューの場合、 // 移動したらハンバーガーを折りたたむ navbarToggler.dispatchEvent(new Event('click')); } else if( e.currentTarget.classList.contains('dropdown-item') && e.currentTarget.closest('.dropdown').classList.contains('show') ) { // 現在の表示がハンバーガーメニューではなく、ドロップダウン内のメニューをクリックした場合 // 移動したらドロップダウンを折りたたむ e.currentTarget.closest('.dropdown').dispatchEvent(new Event('click')); } }); } } </code></pre> <p>やっていることは大体同じです。ただし、いくつか置き換えが必要な部分があったので以下その点について触れていきます。</p> <h2 id="置き換えた部分"><a href="#%E7%BD%AE%E3%81%8D%E6%8F%9B%E3%81%88%E3%81%9F%E9%83%A8%E5%88%86">置き換えた部分</a></h2> <h3 id="複数のクラス指定で要素を取得"><a href="#%E8%A4%87%E6%95%B0%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%B9%E6%8C%87%E5%AE%9A%E3%81%A7%E8%A6%81%E7%B4%A0%E3%82%92%E5%8F%96%E5%BE%97">複数のクラス指定で要素を取得</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://1-notes.com/javascript-multiple-with-getelementsbyclassname/">JavaScript | getElementsByClassNameで複数のclass名を指定して取得する方法 | ONE NOTES</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://q-az.net/none-jquery-find/">脱jQuery .find() | q-Az</a></li> </ul> <pre><code class="js">const elms = document.querySelectorAll('.hoge, .fuga'); </code></pre> <p>jQuery のようにカンマ区切りで <code>document.querySelectorAll()</code> でOK。</p> <h3 id=".on()"><a href="#.on%28%29">.on()</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://q-az.net/without-jquery-on-off/">脱jQuery .on() .off() | q-Az</a></li> </ul> <p>普通に <code>.addEventListener('eventName', function)</code> でOK。</p> <h3 id="複数の要素に対するイベントハンドラ"><a href="#%E8%A4%87%E6%95%B0%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8B%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9">複数の要素に対するイベントハンドラ</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.mitsue.co.jp/knowledge/blog/frontend/201805/24_0917.html">どうして!?document.querySelectorAll(selector).addEventListener()が動かないわけ | フロントエンドBlog | ミツエーリンクス</a></li> </ul> <pre><code class="js">const elms = document.querySelectorAll('.hoge, .fuga'); elms.forEach(elm => { elm.addEventListener('click', process); }, false); </code></pre> <p><code>.querySelectorAll()</code> で取得した要素を <code>.forEach()</code> で反復処理させます。</p> <h3 id="親要素・祖先要素"><a href="#%E8%A6%AA%E8%A6%81%E7%B4%A0%E3%83%BB%E7%A5%96%E5%85%88%E8%A6%81%E7%B4%A0">親要素・祖先要素</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://yuyauver98.me/js-parentnode-closest/">【Javascript】親要素や祖先要素を取得する方法 parentNode\/closest | ゆうやの雑記ブログ</a></li> </ul> <p>普通に <code>.closet('.parent')</code> 。</p> <h3 id="子要素"><a href="#%E5%AD%90%E8%A6%81%E7%B4%A0">子要素</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://webty.jp/staffblog/production/post-1745/">脱jQuery!DOM要素取得コードの素のJavaScriptへの書き換え │ Webty Staff Blog</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://the-zombis.sakura.ne.jp/wp/blog/2019/09/16/post-4014/">【JavaScript】脱JQuery!?メソッドを比べてみた!要素取得編 - Web.fla</a></li> </ul> <p>jQuery の <code>.children('selectorName')</code> は一応プレーンな JavaScript にも <code>.children</code>プロパティ がある模様。</p> <p>ただし、 jQuery のように <code>.children</code> へセレクタ指定はできなさそうなので、この部分は HTML のクラスを子要素の方に付けることで回避しました。</p> <h3 id="クラス名全てを取得"><a href="#%E3%82%AF%E3%83%A9%E3%82%B9%E5%90%8D%E5%85%A8%E3%81%A6%E3%82%92%E5%8F%96%E5%BE%97">クラス名全てを取得</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://shanabrian.com/web/javascript/get-class.php">クラス名(class属性の値)をすべて取得 | JavaScript逆引き | Webサイト制作支援 | ShanaBrian Website</a></li> </ul> <p>jQuery では <code>.prop('class')</code> としていたところですが、 <code>.className</code> でOK。</p> <h3 id="子孫要素の中から探す"><a href="#%E5%AD%90%E5%AD%AB%E8%A6%81%E7%B4%A0%E3%81%AE%E4%B8%AD%E3%81%8B%E3%82%89%E6%8E%A2%E3%81%99">子孫要素の中から探す</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://q-az.net/none-jquery-find/">脱jQuery .find() | q-Az</a></li> </ul> <p>jQueryでは <code>.find()</code> だったところを、 <code>elm.querySelector('selectorName')</code> と指定すればOK。</p> <h3 id="ウインドウ幅"><a href="#%E3%82%A6%E3%82%A4%E3%83%B3%E3%83%89%E3%82%A6%E5%B9%85">ウインドウ幅</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://wemo.tech/470">スクリーン・ウインドウ・画面サイズをjavascriptで取得する方法まとめ | WEMO</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://q-az.net/without-jquery-innerheight-width-outerheight-width/">脱jQuery .innerHeight() .innerWidth() .outerHeight() .outerWidth() | q-Az</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://ja.javascript.info/size-and-scroll-window">ウィンドウサイズとスクローリング</a></li> </ul> <p>jQuery で <code>$(window).outerWidth()</code> としてたところを、今回の用途では <code>window.innerWidth</code> へ置き換えました。</p> <h3 id="クラスの存在チェック"><a href="#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E5%AD%98%E5%9C%A8%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF">クラスの存在チェック</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://q-az.net/none-jquery-addclass-has-remove-toggle/">脱jQuery .addClass() .hasClass() .removeClass() .toggleClass() | q-Az</a></li> </ul> <p>jQuery では <code>.hasClass('className')</code> だったところを、 <code>.classList.contains('className')</code> へ置き換え。</p> <h3 id="イベントハンドラの指定とイベント発火要素の取得"><a href="#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%81%AE%E6%8C%87%E5%AE%9A%E3%81%A8%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E7%99%BA%E7%81%AB%E8%A6%81%E7%B4%A0%E3%81%AE%E5%8F%96%E5%BE%97">イベントハンドラの指定とイベント発火要素の取得</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://note.com/yamanoborer/n/n2e4cc40328b7">【JavaScript】addEventListenerで関数に引数をわたす|北の南|note</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.mitsue.co.jp/knowledge/blog/frontend/201912/02_0000.html">脱jQueryに向けた第一歩 | フロントエンドBlog | ミツエーリンクス</a></li> </ul> <p>普通に関数指定で <code>elm.addEventListener('click', hoge(elm), false)</code> と書いてしまっていました。</p> <p>しかし、参考記事に拠るとこれだと<strong>その関数の実行結果</strong>が渡されるとのこと。しかもイベント発火時ではなく該当コード読み込み時に実行されてしまうため、挙動がおかしくなってしまいます。</p> <p>これについては第二引数はオブジェクト (または JavaScript の純粋な関数) なので <code>elm.addEventListener('click', hoge, false)</code> と関数名だけにしなければならず、その通りに書けばOK。</p> <p>一方、 <code>const hoge = (e) => { /* 処理 */ };</code> で普通にイベントは渡ってくるので、「クリックされた要素」をイベントハンドラ内で利用したい場合は <code>e.currentTarget</code> とすれば問題ないですね。</p> <p>最初これに気付かずしばらく嵌まっていました。参考記事に感謝。</p> <h3 id="イベント発火"><a href="#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E7%99%BA%E7%81%AB">イベント発火</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://mae.chab.in/archives/2821#post2821-5">jQueryを使わない書き方 ajax, each, trigger, on\/off, extend, deferred, animate, css編 | maesblog</a></li> </ul> <p>イベントがクラスで渡す必要がありますがほぼ同じで、 <code>elm.dispatchEvent(new Event('click'))</code> とすればOK。</p> <hr /> <p>このような形でガシガシ書き換えていけば大きな問題はなさそうです。</p> <p>とはいえ、この置き換えは地味にアンカーリンクへのスクロールアニメーションを <code>scroll-behavior: smooth;</code> に移行したおかげで上述以外の大半の JS を破棄しても問題ないと判断できたのが非常に大きいですね。そうでなければもっと大きなボリュームと対峙する必要がありました……。</p> <p>しかも <code>Scroll-margin-top</code> で上部固定ナビゲーションバーの裏側にアンカーリンクが隠れてしまう問題を回避できる、というのも JS コードを削減できた要因の一つなので、この2つのプロパティは個人的にかなり神がかっていると感じます。細かいイージングは犠牲になりますが、今回は全然目を瞑ることができるレベルなので良しとします。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <h3 id="複数のクラス指定で要素を取得"><a href="#%E8%A4%87%E6%95%B0%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%B9%E6%8C%87%E5%AE%9A%E3%81%A7%E8%A6%81%E7%B4%A0%E3%82%92%E5%8F%96%E5%BE%97">複数のクラス指定で要素を取得</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://1-notes.com/javascript-multiple-with-getelementsbyclassname/">JavaScript | getElementsByClassNameで複数のclass名を指定して取得する方法 | ONE NOTES</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://q-az.net/none-jquery-find/">脱jQuery .find() | q-Az</a></li> </ul> <h3 id=".on()"><a href="#.on%28%29">.on()</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://q-az.net/without-jquery-on-off/">脱jQuery .on() .off() | q-Az</a></li> </ul> <h3 id="複数の要素に対するイベントハンドラ"><a href="#%E8%A4%87%E6%95%B0%E3%81%AE%E8%A6%81%E7%B4%A0%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8B%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9">複数の要素に対するイベントハンドラ</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.mitsue.co.jp/knowledge/blog/frontend/201805/24_0917.html">どうして!?document.querySelectorAll(selector).addEventListener()が動かないわけ | フロントエンドBlog | ミツエーリンクス</a></li> </ul> <h3 id="親要素・祖先要素"><a href="#%E8%A6%AA%E8%A6%81%E7%B4%A0%E3%83%BB%E7%A5%96%E5%85%88%E8%A6%81%E7%B4%A0">親要素・祖先要素</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://yuyauver98.me/js-parentnode-closest/">【Javascript】親要素や祖先要素を取得する方法 parentNode\/closest | ゆうやの雑記ブログ</a></li> </ul> <h3 id="子要素"><a href="#%E5%AD%90%E8%A6%81%E7%B4%A0">子要素</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://webty.jp/staffblog/production/post-1745/">脱jQuery!DOM要素取得コードの素のJavaScriptへの書き換え │ Webty Staff Blog</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://the-zombis.sakura.ne.jp/wp/blog/2019/09/16/post-4014/">【JavaScript】脱JQuery!?メソッドを比べてみた!要素取得編 - Web.fla</a></li> </ul> <h3 id="クラス名全てを取得"><a href="#%E3%82%AF%E3%83%A9%E3%82%B9%E5%90%8D%E5%85%A8%E3%81%A6%E3%82%92%E5%8F%96%E5%BE%97">クラス名全てを取得</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://shanabrian.com/web/javascript/get-class.php">クラス名(class属性の値)をすべて取得 | JavaScript逆引き | Webサイト制作支援 | ShanaBrian Website</a></li> </ul> <h3 id="子孫要素の中から探す"><a href="#%E5%AD%90%E5%AD%AB%E8%A6%81%E7%B4%A0%E3%81%AE%E4%B8%AD%E3%81%8B%E3%82%89%E6%8E%A2%E3%81%99">子孫要素の中から探す</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://q-az.net/none-jquery-find/">脱jQuery .find() | q-Az</a></li> </ul> <h3 id="ウインドウ幅"><a href="#%E3%82%A6%E3%82%A4%E3%83%B3%E3%83%89%E3%82%A6%E5%B9%85">ウインドウ幅</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://wemo.tech/470">スクリーン・ウインドウ・画面サイズをjavascriptで取得する方法まとめ | WEMO</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://q-az.net/without-jquery-innerheight-width-outerheight-width/">脱jQuery .innerHeight() .innerWidth() .outerHeight() .outerWidth() | q-Az</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://ja.javascript.info/size-and-scroll-window">ウィンドウサイズとスクローリング</a></li> </ul> <h3 id="クラスの存在チェック"><a href="#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E5%AD%98%E5%9C%A8%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF">クラスの存在チェック</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://q-az.net/none-jquery-addclass-has-remove-toggle/">脱jQuery .addClass() .hasClass() .removeClass() .toggleClass() | q-Az</a></li> </ul> <h3 id="イベントハンドラの指定とイベント発火要素の取得"><a href="#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%81%AE%E6%8C%87%E5%AE%9A%E3%81%A8%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E7%99%BA%E7%81%AB%E8%A6%81%E7%B4%A0%E3%81%AE%E5%8F%96%E5%BE%97">イベントハンドラの指定とイベント発火要素の取得</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://note.com/yamanoborer/n/n2e4cc40328b7">【JavaScript】addEventListenerで関数に引数をわたす|北の南|note</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.mitsue.co.jp/knowledge/blog/frontend/201912/02_0000.html">脱jQueryに向けた第一歩 | フロントエンドBlog | ミツエーリンクス</a></li> </ul> <h3 id="イベント発火"><a href="#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E7%99%BA%E7%81%AB">イベント発火</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://mae.chab.in/archives/2821#post2821-5">jQueryを使わない書き方 ajax, each, trigger, on\/off, extend, deferred, animate, css編 | maesblog</a></li> </ul> <h3 id="その他"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96">その他</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/nightyknite/items/668c112c40931515ed67">JQueryをVanilla JSに緩やかに置き換える - Qiita</a></li> </ul> <h3 id="Bootstrap 5 のブレークポイント"><a href="#Bootstrap+5+%E3%81%AE%E3%83%96%E3%83%AC%E3%83%BC%E3%82%AF%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88">Bootstrap 5 のブレークポイント</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://bootstrap-guide.com/components/navbar">ナビゲーションバー~Bootstrap5設置ガイド</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://bootstrap-guide.com/layout/breakpoints">ブレークポイント~Bootstrap5設置ガイド</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/18175 2022-04-26T17:01:11+09:00 2022-05-04T13:28:56+09:00 https://crieit.net/posts/493a752726fa6c47c162fcb7379c2c6d 気象庁の公開データを node.js と chrome ブラウザ (browserify) で表示する。 <p><a target="_blank" rel="nofollow noopener" href="https://rentry.co/nc73d">https://rentry.co/nc73d</a></p> <p>雨なので気象庁の公開データを Javascript を使って見てみる。</p> <h2 id="pathCode (json) を表示してみる"><a href="#pathCode+%28json%29+%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">pathCode (json) を表示してみる</a></h2> <p>気象データを問い合わせるのに、定数になっている日本全国の場所のコード( pathCode )を見てみる。<br /> なぜなら、それをしらないとエリアごとの気象データを問い合わせできない。</p> <h3 id="javascript"><a href="#javascript">javascript</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://ipfs.infura.io/ipfs/QmPrjHjXe6pSwh39Qjh326uLQWsoWtBJqNvdtPnMxsRDHq"><code>tenki_area.js</code></a></p> <pre><code class="javascript">const axios = require("axios"); const url = "https://www.jma.go.jp/bosai/common/const/area.json"; (get_area = async () => { try { const response = await axios.get(url); console.log(response.data); } catch (error) { console.error(error); } })(); </code></pre> <p>terminal:</p> <pre><code class="bash">> node tenki-area.js > tenki-area.json </code></pre> <p>Rf:<br /> <a target="_blank" rel="nofollow noopener" href="https://mindtech.jp/?p=1754">https://mindtech.jp/?p=1754</a></p> <h3 id="Ruby"><a href="#Ruby">Ruby</a></h3> <pre><code class="ruby">require "json" require "net/http" area_code_url = "https://www.jma.go.jp/bosai/common/const/area.json" response = Net::HTTP.get_response(URI.parse(area_code_url)) h_area = JSON.parse(response.body) pp h_area["offices"] </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://ipfs.infura.io/ipfs/QmT3YkWhro7xMQ3uNwF4QNXUnt6aAjoGuaupzKg5ZtdqNs">tenki_area.rb</a></p> <pre><code class="ruby">require "json" require "net/http" area_code_url = "https://www.jma.go.jp/bosai/common/const/area.json"; response1 = Net::HTTP.get_response(URI.parse(area_code_url)) h_area_code = JSON.parse(response1.body) uri = "https://www.jma.go.jp/bosai/forecast/data/forecast/" #area = "260000" #Kyoto h_area_code["offices"].each {|v| area = v[0] begin response2 = Net::HTTP.get_response(URI.parse("#{uri}#{area}.json")) h_area_data = JSON.parse(response2.body) h_area_data.each {|vv| pp vv["publishingOffice"] pp vv["reportDatetime"] vv["timeSeries"].each {|x| x["areas"].each {|xx| pp xx['area']} } puts puts "--------------------------------------------------" puts } rescue puts end } </code></pre> <h3 id="Python"><a href="#Python">Python</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://ipfs.infura.io/ipfs/Qmat48qKAeh6LovrqfzdisYBhbAtmKb5dpPHXZvxrisM7P">tenki_area.py</a></p> <pre><code class="python">import requests import json area_code_url = "https://www.jma.go.jp/bosai/common/const/area.json"; headers = {"content-type": "application/json; charset=utf-8"} response = requests.get(area_code_url, headers=headers) data = response.json() print(json.dumps(data['offices'],ensure_ascii=False,indent = 4)) </code></pre> <h3 id="Nim"><a href="#Nim">Nim</a></h3> <pre><code class="nim">#nim -d:ssl import httpclient,strutils,uri import json const area_code_url = "https://www.jma.go.jp/bosai/common/const/area.json" let client0 = newHttpClient() let response = client0.get(area_code_url) let jsObj = parseJson(response.body) client0.close() echo jsObj["offices"].pretty() </code></pre> <h2 id="get weather data"><a href="#get+weather+data">get weather data</a></h2> <p>Rf:<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/youtoy/items/932bc48b03ced5a45c71">https://qiita.com/youtoy/items/932bc48b03ced5a45c71</a></p> <h3 id="Ruby"><a href="#Ruby">Ruby</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://ipfs.infura.io/ipfs/QmPsnpodrqshrnwJAd3AjQdgmzxHcoWvTa6F1UZVYPjjxq">tenki.rb</a></p> <p>例えば京都という場所の天気を問い合わせる。</p> <h3 id="javascript"><a href="#javascript">javascript</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://ipfs.infura.io/ipfs/QmZBPSgQwSspdQeaFpCCo9fPfrrbKfZi1A3j6aW8cY8W6Y"><code>tenki.js</code></a></p> <pre><code class="javascript">const axios = require("axios"); const url = "https://www.jma.go.jp/bosai/forecast/data/forecast/"; const area = "260000"; // Kyoto (getWeatherForecast = async () => { try { const response = await axios.get(`${url}${area}.json`); console.log(response.data); console.log(response.data[0].publishingOffice); console.log(response.data[0].timeSeries[0].areas); for(const area of response.data[0].timeSeries[0].areas){ console.log(`----${area.area.name}----`); for(const weather of area.weathers){ console.log(weather); } } } catch (error) { console.error(error); } })(); </code></pre> <p>terminal:</p> <pre><code class="bash">> node tenki.js </code></pre> <pre><code>> node tenki.js [ { publishingOffice: '京都地方気象台', reportDatetime: '2022-04-26T17:00:00+09:00', timeSeries: [ [Object], [Object], [Object] ] }, { publishingOffice: '京都地方気象台', reportDatetime: '2022-04-26T17:00:00+09:00', timeSeries: [ [Object], [Object] ], tempAverage: { areas: [Array] }, precipAverage: { areas: [Array] } } ] 京都地方気象台 [ { area: { name: '南部', code: '260010' }, weatherCodes: [ '300', '311', '100' ], weathers: [ '雨 所により 夜遅く 雷を伴い 激しく 降る', '雨 昼前 から くもり 後 晴れ 所により 明け方 まで 雷を伴い 激しく 降る', '晴れ' ], winds: [ '南の風 後 南西の風', '南西の風 後 北西の風', '北の風' ] }, { area: { name: '北部', code: '260020' }, weatherCodes: [ '300', '311', '100' ], weathers: [ '雨 所により 夜遅く 雷を伴い 激しく 降る', '雨 昼前 から くもり 後 晴れ 所により 明け方 まで 雷を伴い 激しく 降る', '晴れ' ], winds: [ '南の風 やや強く 海上 では 南の風 強く', '南西の風 後 北東の風  海上 では 南西の風 やや強く', '北の風' ], waves: [ '1.5メートル 後 2メートル', '2メートル 後 1.5メートル', '1 .5メートル' ] } ] ----南部---- 雨 所により 夜遅く 雷を伴い 激しく 降る 雨 昼前 から くもり 後 晴れ 所により 明け方 まで 雷を伴い 激しく 降る 晴れ ----北部---- 雨 所により 夜遅く 雷を伴い 激しく 降る 雨 昼前 から くもり 後 晴れ 所により 明け方 まで 雷を伴い 激しく 降る 晴 </code></pre> <p>これをサンプルにして Javascript を学習する。</p> <h2 id="browserify"><a href="#browserify">browserify</a></h2> <p>node.js で動いたプログラムを browserify によってブラウザで動作するようにする。</p> <p><a target="_blank" rel="nofollow noopener" href="https://browserify.org/">https://browserify.org/</a></p> <blockquote> <p>Browsers don't have the require method defined, but Node.js does. With Browserify you can write code that uses require in the same way that you would use it in Node.</p> </blockquote> <p>インストール<br /> terminal:</p> <pre><code>> npm install -g browserify </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://ipfs.infura.io/ipfs/QmW7gBLMnDdpEy4rp9NVp5jWnmsFwoxYyvSKA1y5fXtVzd"><code>tenki_2.js</code></a></p> <p>他の言語である println のように使っているコンソール.ログがブラウザでどうなるのかわからないので、require しておいてみる。</p> <pre><code class="javasript">const console = require("console"); const axios = require("axios"); const url = "https://www.jma.go.jp/bosai/forecast/data/forecast/"; const area = "260000"; // Kyoto (getWeatherForecast = async () => { try { const response = await axios.get(`${url}${area}.json`); // console.log(response.data); // console.log(response.data[0].publishingOffice); // console.log(response.data[0].timeSeries[0].areas); for(const area of response.data[0].timeSeries[0].areas){ console.log(`----${area.area.name}----`); for(const weather of area.weathers){ console.log(weather); } } } catch (error) { console.error(error); } })(); </code></pre> <p><code>tenki_2.js</code> をブラウザ仕様の javascript ファイル <code>bundle.js</code> に変換する。</p> <p>terminal:</p> <pre><code class="bash">> browserify tenki_2.js -o bundle.js </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://ipfs.infura.io/ipfs/Qmd3cJNFTft8gbA444XgefmWXFGvY4QECVqYyicQ1dbs5C"><code>bundle.js</code></a><br /> <a target="_blank" rel="nofollow noopener" href="https://pastebin.com/szscGNsR">https://pastebin.com/szscGNsR</a></p> <p>html ファイルを作って、 bundle.js を読み込むようにする。<br /> 例えば tenki という名前でフォルダを作って、index.html, bundle.js を tenki フォルダに配置する。</p> <p><a target="_blank" rel="nofollow noopener" href="https://ipfs.infura.io/ipfs/QmW5oGdEBSJJpazfds7LSqyMttxNYj8BVBphtfJYpP7wWv"><code>index.html</code></a></p> <pre><code class="html"><!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>tenki_area browserify example</title> <script src="bundle.js"></script> </head> <body> </body> </html> </code></pre> <p>chrome ブラウザーで tenki フォルダの中の html ファイルを開く。</p> <p>chrome browser:</p> <p><code>CTRL</code> + <code>shift</code> + <code>i</code></p> <p>DevTools</p> <p><code>index.html</code>:</p> <p><a href="https://crieit.now.sh/upload_images/d6edfe05648ad36e61e2c39bcce18cb06267ee2adf460.JPG" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d6edfe05648ad36e61e2c39bcce18cb06267ee2adf460.JPG?mw=700" alt="'chromebrowser developper tools'" /></a></p> tomato tag:crieit.net,2005:PublicArticle/17948 2022-01-20T19:00:13+09:00 2022-01-20T19:00:13+09:00 https://crieit.net/posts/Dart-61e932ada423e Dartを使った大規模なアプリ開発 <p>こんにちは、Quireです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quire</a>は、Dartで開発された初めてのWebアプリケーションではありませんが、クライアントとサーバーの両方でDartをふんだんに使ったものとしては、初めてかもしれません。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/1a086b990119-20220120.png" alt="" /></p> <p>動作の軽い、徹底して階層構造のタスク管理ツールです。このプロジェクトはDartコードで合計53992行、1620 KB。コミュニティーからのオープンソース ライブラリも使っています。</p> <h1 id="私たちについて"><a href="#%E7%A7%81%E3%81%9F%E3%81%A1%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">私たちについて</a></h1> <p>このプロジェクトを開始するまで、私たちはRikuloというDartファンの集まりでした。Rikuloはこれまで、UIフレームワークやUIライブラリ、Webサーバー、メッセージサーバー、DBクライアントなどのDartライブラリをリリースしています。<br /> Dartが2011年に初めてリリースされたとき、すぐに将来性を感じ、さっそく小規模のプロジェクトに着手。その後、ほとんどDartで書かれた大規模アプリの開発へと進みました。今回はそのときの経験をお話しします。Dartによる開発に興味を持っていただければ幸いです。</p> <h1 id="なぜDartか"><a href="#%E3%81%AA%E3%81%9CDart%E3%81%8B">なぜDartか</a></h1> <p>私たちがDartを選んだ理由はたくさんありますが、以下に大きなものを挙げてみます。<br /> - 強く型付けされていて、無数の小さなミスを防ぐことができる。公式IDEのDart Editorは、タイプミスへの即時フィードバックやオートコンプリート機能を備えており、トラッキングも容易。<br /> - 継承がクラスベースで、プロトタイプベースよりも直感的に使える。<br /> - クライアントとサーバーの両方で同じ言語が使えて、データモデルやコードベースを共有できる。<br /> - JavaScriptの問題点をほぼ解消。完全ではないが、以前にあった問題の99%には対応している。<br /> - JavaScript Harmonyで人気の特徴をいくつか備えている。例えばFuture(Promise)パターン、矢印関数など。<br /> - 強力なチームの後ろ盾があり、ハイクオリティーな公式ライブラリや使いやすいAPIを利用できる。まだ開発途上ながら、仕様はすでにかなり安定している。<br /> - サーバーサイドではマルチスレッドではなくイベントループ方式で、好みだった。<br /> - JavaScriptへのコンパイル時にTree Shakingできる(下記参照)。</p> <h5 id="もちろんデメリットもあります。以下に挙げてみます。"><a href="#%E3%82%82%E3%81%A1%E3%82%8D%E3%82%93%E3%83%87%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88%E3%82%82%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82%E4%BB%A5%E4%B8%8B%E3%81%AB%E6%8C%99%E3%81%92%E3%81%A6%E3%81%BF%E3%81%BE%E3%81%99%E3%80%82">もちろんデメリットもあります。以下に挙げてみます。</a></h5> <ul> <li>コミュニティーがJavaScriptよりも圧倒的に小さい。</li> <li>DartとJavaScript間の通信が自明でない。</li> <li>強い型付けなので、APIのPolyfillが面倒。</li> <li>Dart Editorの性能が(今のところ)大規模プロジェクトに適さない。</li> </ul> <p>強い型のサポートにより、書くプロセスがJavaScriptよりもずっとロバストです。また、Javaよりもシンプルで、Vanilla JavaScriptと比べてもシンプルなことがあります(関数の書き方など)。全体的にDartは扱いやすい言語ですが、以下のような例外はあります。</p> <ul> <li>コンストラクタのボディよりも先にイニシャライザでFinalフィールドを初期化する必要がある。</li> <li>ミックスインの仕様が使いにくく、2.0まで改善されそうにない。</li> <li>ジェネリック型パラメータがクラスにしかなく、関数宣言にない。(そのためコンパイラの負荷が増す)。</li> </ul> <h2 id="クライアントサイド"><a href="#%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%82%B5%E3%82%A4%E3%83%89">クライアントサイド</a></h2> <p>Dart VMがChromeに統合される日まで、JavaScriptにコンパイルして作成することになります。JavaScriptにコンパイルする言語はたくさんありますが、Dartには以下のような独自のメリットがあります。</p> <ul> <li>私たちは開発時、Dart VMを内蔵したChromiumベースのブラウザー、DartiumでDartをネイティブに実行している。このイテレーションではコンパイルが不要で、スムーズに進めることができる。</li> <li>テストとプロダクションの段階でJavaScriptにコンパイルし、すべてのメジャーなブラウザーで実行する。このとき使用するコンパイラがTree Shakingを行い、不要なコードを除去するため、JavaScriptのコードサイズを小さくできる。</li> </ul> <h3 id="サーバーサイド"><a href="#%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%B5%E3%82%A4%E3%83%89">サーバーサイド</a></h3> <p>サーバーサイドについてはあまりコミュニティーで話題になりませんが、私たちは以下の理由から、Dartはサーバーサイドのプログラミング言語に入ると考えています。</p> <ul> <li>Webサービスは本質的に非同期なため、イベントループ方式と相性がいい(マルチスレッドと比べて)。</li> <li>クライアントサイドよりもサーバーサイドのほうが、よりロバストで安全なコードが必要。強い型付けの言語はこの点において有利。</li> </ul> <h1 id="サポートライブラリ"><a href="#%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA">サポートライブラリ</a></h1> <p>Quireは約30のライブラリをインポートしています。そのうち10ライブラリはコミュニティーからで、残りはDartからリリースされたものです。Dartに詳しい方ならAngularDARTとPolymer.dartがあると思うかもしれませんが、実はどちらも使っていません。</p> <h5><a target="_blank" rel="nofollow noopener" href="https://angulardart.org/">AngularDART</a>を使わない理由は、以下のとおりです。</h5> <ul> <li>DOMを細かく制御したい。</li> <li>私たちはクライアント構造の構築に独自のアーキテクチャガイドラインを使用しており、そのパラダイムがAngularのロジックと異なる。</li> <li>AngularDARTについて調べたとき、コンパイルされたJavaScriptのコードサイズにかなりのオーバーヘッドが生じた。ただし、これはその後かなり改善された。</li> </ul> <h5 id="私たちは以下の理由から、Polymer.dartも使っていません。"><a href="#%E7%A7%81%E3%81%9F%E3%81%A1%E3%81%AF%E4%BB%A5%E4%B8%8B%E3%81%AE%E7%90%86%E7%94%B1%E3%81%8B%E3%82%89%E3%80%81Polymer.dart%E3%82%82%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%81%BE%E3%81%9B%E3%82%93%E3%80%82">私たちは以下の理由から、Polymer.dartも使っていません。</a></h5> <ul> <li>カプセル化とイベント リターゲティングのため、ShadowDOMはBootstrapのようなセレクター指向のフレームワークと併用できない。</li> <li>ShadowDOMの外部からスタイルを適用できない。サードパーティーのコンポーネントセットがPolymerで作成されている場合、ユーザーが見た目や感じを変えることは不可能に近い。(更新:2013年12月時点で、内部のスタイルを外部から変更可能。<a target="_blank" rel="nofollow noopener" href="http://www.html5rocks.com/en/tutorials/webcomponents/shadowdom-201/#toc-style-cat-hat">Shadow DOM 201</a>を参照。)</li> </ul> <h3 id="DQuery&Bootjack"><a href="#DQuery%EF%BC%86Bootjack">DQuery&Bootjack</a></h3> <p>私たちのクライアントサイド スタックの基盤は、Rikuloリリースのオープンソース プロジェクト、DQueryとBootjackです。<br /> - <a target="_blank" rel="nofollow noopener" href="https://github.com/rikulo/dquery">DQuery</a>はjQueryの一部、特にイベントデリゲートの仕組みの部分をDartに移植したものです。<br /> - <a target="_blank" rel="nofollow noopener" href="https://github.com/rikulo/bootjack">Bootjack</a>はBootstrap 3の完全な移植で、CSS およびAPIとほぼ同じです。</p> <p>このようにアプリケーション スタックを構築して、JavaScriptについて知っていること、できることを活用しています。</p> <h3 id="Stream"><a href="#Stream">Stream</a></h3> <p>私たちのWebサーバーは、Dartのみで書かれ、ルーティング、フィルター、サーバーサイドMVCなどを備えたStreamです。イベントループ方式とシームレスに連携して動作します。リクエストハンドラーも、ノンブロッキングのルーチンをつなげるだけで書くことができ、従来のマルチスレッド方式よりも生産的かつ快適に仕事ができます。私たちは、Webサービスをスケールし、nginxでHTTPSを処理し、Streamにリクエストをデリゲートしたりもしています。このアーキテクチャによりDart VMを個別にSpawn/Despawnでき、サーバーのアップグレードもユーザーの使用を妨げずに実施できます。</p> <h1 id="終わりに"><a href="#%E7%B5%82%E3%82%8F%E3%82%8A%E3%81%AB">終わりに</a></h1> <p>Dartは使い勝手の良い言語です。この記事がDartコミュニティー成長の一助となることを願っています。最後に、Dartで何ができるか興味のある方は、ぜひ<a target="_blank" rel="nofollow noopener" href="https://quire.io">Quire</a>を試してみてください。</p> uniyeh tag:crieit.net,2005:PublicArticle/17627 2021-08-30T17:14:25+09:00 2021-08-30T17:14:25+09:00 https://crieit.net/posts/GitHub-Actions-Jest GitHub ActionsでJestを叩くときのタイムゾーンと言語を設定する <p>GitHub リポジトリにプッシュした時、GitHub Actions のワークフローで自動的に Jest を走らせるようにしています。GitHub 上で自動的に走る Jest と、手動で直接叩く Jest との違いはタイムゾーンとロケールです。ここでは、GitHub 上で Jest を動かすときに環境変数としてタイムゾーンとロケールを指定する方法について説明します。</p> <h2 id="1. package.json にグローバル設定ファイルを指定する"><a href="#1.+package.json+%E3%81%AB%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E8%A8%AD%E5%AE%9A%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B">1. package.json にグローバル設定ファイルを指定する</a></h2> <p>以下のように設定を追記します。<code>jest-global-setup.js</code>に設定を記載すれば、環境変数から読み込まれた値を上書きできます。</p> <pre><code class="javascript"> "jest": { "globalSetup": "./jest-global-setup.js" }, </code></pre> <h2 id="2. jest-global-setup.js の中身を記載する"><a href="#2.+jest-global-setup.js+%E3%81%AE%E4%B8%AD%E8%BA%AB%E3%82%92%E8%A8%98%E8%BC%89%E3%81%99%E3%82%8B">2. jest-global-setup.js の中身を記載する</a></h2> <p>タイムゾーンと言語を指定します。</p> <pre><code class="javascript">module.exports = async () => { process.env.TZ = "Asia/Tokyo"; process.env.LANG = "en_US.UTF-8"; }; </code></pre> <h2 id="3. 動作結果を確認する"><a href="#3.+%E5%8B%95%E4%BD%9C%E7%B5%90%E6%9E%9C%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B">3. 動作結果を確認する</a></h2> <p>Jest のテストで出力される日時の文字列を確認したところ、言語が日本語から英語になっていることが確認できました。</p> <pre><code class="text"> - "date": "Wed Apr 01 2020 09:00:00 GMT+0900 (日本標準時)", + "date": "Wed Apr 01 2020 09:00:00 GMT+0900 (Japan Standard Time)", </code></pre> kabueye tag:crieit.net,2005:PublicArticle/17600 2021-08-14T18:58:28+09:00 2021-08-14T18:58:28+09:00 https://crieit.net/posts/GAS-611793c436908 【分かる方教えてください】GASで、スプレッドシートに入力した値と、現在の日(年・日時ではなく、「日」のみの値が合致しているかどうか) が同じであれば、チャットワークにその値を表示する というプログラムを作成しています。 <p>プログラム自体にエラーは出ていないのですが、スプレッドシートに「14」と入力しているけれど(本日の日と合致しているけれど)、<br /> チャットワークにelseの値(15)がに表示されてしまうため、正しく日を拾えていないかと思います。<br /> 理由が分かれば教えていただきたいです。よろしくお願いします。</p> <p>以下コード</p> <p>===</p> <p>function myFunction() {</p> <p>//スプレッドシートの、特定の範囲を読み込む<br /> var sheet = SpreadsheetApp.getActiveSheet();<br /> var x = sheet.getRange(2,1).getValue();</p> <p>//日付の定義(日を取得する)<br /> var date = new Date<br /> var day = date.getDate();<br /> var yesterday = date.getDate(day-1);<br /> var tomorrow = date.getDate(day+1);</p> <p>//入力した日と条件が合致していれば、(CWに値を表示する)<br /> if( x > yesterday && x < tomorrow)<br /> {</p> <p>x=x;</p> <p>}</p> <p>else{</p> <p>x=x+1;</p> <p>}</p> <p>var client = ChatWorkClient.factory({token: '8314e1a8f4c4e0c5891d1d90f731e85b'});<br /> client.sendMessage({room_id:100266334, body:x});</p> <p>}</p> tanakaQiita tag:crieit.net,2005:PublicArticle/17548 2021-07-31T17:37:19+09:00 2021-07-31T17:37:19+09:00 https://crieit.net/posts/cypres-bij-sterrennacht-vrtest-20210731 Cypress + reg-cli でビジュアルリグレッションテスト環境を作る <h2 id="経緯"><a href="#%E7%B5%8C%E7%B7%AF">経緯</a></h2> <ul> <li><a href="https://crieit.net/posts/backstopjs-test-20201122">BackstopJS を試す (Error: Failed to launch the browser process! エラー発生→ puppeteer のバージョンを指定して解決) - Crieit</a></li> <li><a href="https://crieit.net/posts/backstop-trial-and-error-20201215">BackstopJS で背景画像の高さを `vh` 単位で指定したページで画像やスクリーンショットが引き伸ばされる現象についてメモ (未解決) - Crieit</a> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/arm-band/test_backstop_randa_back_stopper_2">arm-band/test_backstop_randa_back_stopper_2: BackstopJS のテストです。</a></li> </ul></li> </ul> <p>BackstopJS が上手く行かなかったので Cypress を使う方法に切り替えたところ、わりと上手く行きそうだったのでこちらの方法に切り替えてみました。</p> <h2 id="成果物"><a href="#%E6%88%90%E6%9E%9C%E7%89%A9">成果物</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/arm-band/cypres_bij_sterrennacht">GitHub - arm-band/cypres_bij_sterrennacht: Cypress + reg-cli によるビジュアルリグレッションテストのスクリプト。</a></li> </ul> <p>スクリプトのベースは</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/yodyman/items/3a2fbb106eaaca3aca47">Cypress + REG CLI ではじめたビジュアルリグレッションテスト - Qiita</a></li> </ul> <p>こちらを参考に、自分で使いやすいようにさらにいくつかチョイ足しをした感じになります。</p> <h2 id="構成"><a href="#%E6%A7%8B%E6%88%90">構成</a></h2> <h3 id="ディレクトリ構造"><a href="#%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E6%A7%8B%E9%80%A0">ディレクトリ構造</a></h3> <pre><code>PROJECT_ROOT/ ├ cypress/ │ ├ fixtures/ │ │ └ visual-regression-test/ │ │ └ config.json.sample // 設定サンプル │ │ │ ├ integration/ │ │ └ visual-regression-test/ │ │ ├ cafe_bij_nacht-before.spec.js // ref のキャプチャを取得するスクリプト │ │ └ cafeterras_bij_nacht-after.spec.js // test のキャプチャを取得するスクリプト │ │ │ ├ plugins/ │ │ └ index.js // イベントにフックさせて動作させるスクリプト │ │ │ └ support/ // 略 │ ├ package.json ├ sterrennacht.js // Cypress + reg-cli のチェック後に reg-cli のレポートのHTMLを開く └ zonnebloemen.js // Cypress 実行時の設定パラメータを調整する </code></pre> <h3 id="package.json"><a href="#package.json">package.json</a></h3> <pre><code class="json">{ "name": "cypres_bij_sterrennacht", "version": "0.0.1", "description": "Cypress + reg-cli によるビジュアルリグレッションテストのスクリプト。", "main": "sterrennacht.js", "scripts": { "result": "node ./sterrennacht", "cypress:before": "cypress run --config-file false --config trashAssetsBeforeRuns=true -s cypress/integration/visual-regression-test/cafe_bij_nacht-before.spec.js", "cypress:after": "cypress run --config-file false --config trashAssetsBeforeRuns=false -s cypress/integration/visual-regression-test/cafeterras_bij_nacht-after.spec.js", "cypress": "run-s cypress:*", "reg": "reg-cli ./cypress/screenshots/visual-regression-test/before ./cypress/screenshots/visual-regression-test/after ./cypress/screenshots/visual-regression-test/diff -R ./cypress/screenshots/visual-regression-test/report/index.html -M 0.2 -I", "test": "run-s cypress:* reg result" }, "author": "Arm Band", "license": "ISC", "dependencies": { "browser-sync": "^2.26.14", "cypress": "^7.6.0", "npm-run-all": "^4.1.5", "opener": "^1.5.2", "reg-cli": "^0.17.0" } } </code></pre> <p>npm scripts にタスクを記述、 <code>yarn test</code> で以下の4つのタスクを一括で動かします。</p> <ol> <li>Cypress による変更<strong>前</strong>サイトのキャプチャの撮影</li> <li>Cypress による変更<strong>後</strong>サイトのキャプチャの撮影</li> <li><code>reg-cli</code> で1., 2.の比較とレポート生成</li> <li>レポートのHTMLを <code>openner</code> で開く</li> </ol> <h3 id="zonnebloemen.js"><a href="#zonnebloemen.js">zonnebloemen.js</a></h3> <pre><code class="javascript">/** * zonnebloemen : 設定を生成する */ class zonnebloemen { /** * getParameters : 設定用のフラグを生成 * * @param {booelan} flag : フラグ * * @return {Object} : 設定パラメータの内容 */ getParameters (flag) { return flag === 'BEFORE' ? { outStr: 'before', urlKey: 'ref', flag : true, } : { outStr: 'after', urlKey: 'test', flag : false, }; }; /** * getConfig : 設定を生成 * * @param {Object} data : JSONファイル のデータ * @param {booelan} flag : フラグ * * @return {Object} : 設定の内容 */ getConfig (data, flag) { if (data !== undefined && data !== null && Object.keys(data).length > 0) { flag ? data.commons['trashAssetsBeforeRuns'] = true : data.commons['trashAssetsBeforeRuns'] = false; } return data; }; }; module.exports = zonnebloemen; </code></pre> <p>フラグによって変更前なのか変更後なのか、スクリプトを実行する際の設定の配列のパラメータを弄るクラス。</p> <p>具体的には以下の2つ。</p> <ul> <li>変更前 <ul> <li>参照先URL: <code>ref</code>キー にセットされた値</li> <li><code>trashAssetsBeforeRuns</code>(キャプチャ取得スクリプト実行前にキャプチャ格納ディレクトリをクリーンアップするか): <code>true</code></li> </ul></li> <li>変更後 <ul> <li>参照先URL: <code>test</code>キー にセットされた値</li> <li><code>trashAssetsBeforeRuns</code>: <code>false</code></li> </ul></li> </ul> <h3 id="config.json.sample"><a href="#config.json.sample">config.json.sample</a></h3> <pre><code class="json">{ "commons": { "url": { "ref": "https://www.example.jp", "test": "https://demo.example.jp" }, "hideElements": [ "#__bs_notify__", ".returnPageTop", ".c-returnPageTop", "#navbar", ".navbar" ], "screenshot": { "capture": "fullPage", "waitMsec": 1000 } }, "viewports": [ { "name": "phone", "width": 375, "height": 667 }, { "name": "tablet", "width": 1024, "height": 768 }, { "name": "pc", "width": 1920, "height": 1080 } ], "pages": [ { "test_id": "top", "uri": "/" }, { "test_id": "about", "uri": "/about.html" } ] } </code></pre> <p>サンプルですがやりたいことは最低限押さえてあるかと。</p> <ul> <li><code>commons</code>: <ul> <li><code>url</code>: <ul> <li><code>ref</code>: お手本として参照するサイトのURL</li> <li><code>test</code>: テストしたいサイトのURL</li> </ul></li> </ul></li> <li><code>viewports</code>: デバイス(ブラウザ)の幅と高さ。デフォルトではスマートフォン、タブレット、PCとして3つのサイズを指定</li> <li><code>pages</code>: テストしたいページのURLとキャプチャのファイル名に使用するID</li> </ul> <p>基本的にこの設定に従って Cypress でキャプチャを撮っていきます。</p> <h3 id="cafe_bij_nacht-before.spec.js"><a href="#cafe_bij_nacht-before.spec.js">cafe_bij_nacht-before.spec.js</a></h3> <pre><code class="javascript">const flag = 'BEFORE'; const configDataOrigin = require('../../fixtures/visual-regression-test/config.json'); const zonnebloemenClass = require('../../../zonnebloemen'); const zonnebloemen = new zonnebloemenClass(); const parameters = zonnebloemen.getParameters(flag); const configData = zonnebloemen.getConfig(configDataOrigin, parameters.flag); describe(`${parameters.outStr} screenshot`, function () { const pages = configData.pages; const domain = configData.commons.url[parameters.urlKey]; const viewports = configData.viewports; const ss_dir = parameters.outStr; var test_id = ''; var ss_path = ''; var url = ''; pages.forEach(({ test_id, uri }) => { viewports.forEach(({ name, width, height }) => { context(test_id, () => { beforeEach(() => { url = domain + uri; cy.visit(url); }); it('take screenshot', function () { ss_path = ss_dir + '/' + test_id + '-' + name; cy.wait(configData.commons.screenshot.waitMsec); cy.viewport(width, height); cy.scrollTo('bottom'); cy.screenshot(ss_path, { onBeforeScreenshot($el) { for (const selector of configData.commons.hideElements) { const $selector = $el.find(selector); if ($selector) { $selector.hide(); } } }, capture: Cypress.env(configData.commons.screenshot.capture), }); }); }); }); }); }); </code></pre> <p>最初の定数 <code>flag</code> 以外は <code>cafe_bij_nacht-before.spec.js</code> と <code>cafeterras_bij_nacht-after.spec.js</code> で共通となります。本当は1つのファイルにしたかったのですが上手く動かなかったので2つにしました。</p> <p>やっていることは <code>flag</code> によって <code>zonnebloemen.js</code> で設定パラメータの配列を生成、その値を以てキャプチャを撮影する、というもの。</p> <h3 id="cafeterras_bij_nacht-after.spec.js"><a href="#cafeterras_bij_nacht-after.spec.js">cafeterras_bij_nacht-after.spec.js</a></h3> <pre><code class="javascript">const flag = 'AFTER'; const configDataOrigin = require('../../fixtures/visual-regression-test/config.json'); const zonnebloemenClass = require('../../../zonnebloemen'); const zonnebloemen = new zonnebloemenClass(); const parameters = zonnebloemen.getParameters(flag); const configData = zonnebloemen.getConfig(configDataOrigin, parameters.flag); describe(`${parameters.outStr} screenshot`, function () { const pages = configData.pages; const domain = configData.commons.url[parameters.urlKey]; const viewports = configData.viewports; const ss_dir = parameters.outStr; var test_id = ''; var ss_path = ''; var url = ''; pages.forEach(({ test_id, uri }) => { viewports.forEach(({ name, width, height }) => { context(test_id, () => { beforeEach(() => { url = domain + uri; cy.visit(url); }); it('take screenshot', function () { ss_path = ss_dir + '/' + test_id + '-' + name; cy.wait(configData.commons.screenshot.waitMsec); cy.viewport(width, height); cy.scrollTo('bottom'); cy.screenshot(ss_path, { onBeforeScreenshot($el) { for (const selector of configData.commons.hideElements) { const $selector = $el.find(selector); if ($selector) { $selector.hide(); } } }, capture: Cypress.env(configData.commons.screenshot.capture), }); }); }); }); }); }); </code></pre> <p>上述で説明した通りなので省略。</p> <h3 id="plugins/index.js"><a href="#plugins%2Findex.js">plugins/index.js</a></h3> <pre><code class="javascript">/// <reference types="cypress" /> // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) /** * @type {Cypress.PluginConfig} */ // eslint-disable-next-line no-unused-vars const path = require('path'); const fs = require('fs'); const isExistFile = (file) => { try { fs.statSync(file); return true; } catch (err) { if (err.code === 'ENOENT') { return false; } } }; module.exports = (on, config) => { on('after:screenshot', (details) => { const str = details.path; const newPath = str.replace(/[\w\d_]+\-(before|after)\.spec\.js/, ''); const newDir = path.dirname(newPath); fs.mkdirSync(newDir, { recursive: true }); fs.renameSync(details.path, newPath); return { path: newPath }; }); on('after:spec', (details) => { const newPath = details.absolute.replace('integration', 'screenshots'); newPath.match(/[\w\d_]+\-(before|after)\.spec\.js/); const childNewPath = path.join(newPath, RegExp.$1); if (isExistFile(childNewPath)) { fs.rmdirSync(childNewPath); } if (isExistFile(newPath)) { fs.rmdirSync(newPath); } }); }; </code></pre> <p>通常だとキャプチャの保存先のディレクトリ名がキャプチャ撮影スクリプトのファイル名そのものになってしまい、ディレクトリ名にドットが入るなどややもやっとする形式なので、そこを <code>before</code> or <code>after</code> のみになるように保存先ディレクトリのリネーム (ファイルコピー+オリジナルディレクトリ削除) を実行するようにしています。</p> <h3 id="sterrennacht.js"><a href="#sterrennacht.js">sterrennacht.js</a></h3> <pre><code class="javascript">const opener = require('opener'); const dir = { baseDir: './cypress/screenshots/visual-regression-test/', startPath: 'report/index.html', }; opener(`${dir.baseDir}${dir.startPath}`); </code></pre> <p><code>opener</code> で開くだけの簡単なスクリプト。</p> <h2 id="実行結果(サンプル)"><a href="#%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C%28%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%29">実行結果(サンプル)</a></h2> <p>試しに少し動かしてみます。サンプルは<a target="_blank" rel="nofollow noopener" href="https://ususama.ewigleere.net/">ホーム | Kiribi Ususama</a>で、公開されているものを <code>ref</code> 、ローカルで Browsersync で開いたものを <code>test</code> として動作させてみます。</p> <p><a href="https://crieit.now.sh/upload_images/b8395bd7e1285b90b36516b0f4eaf57161050ba36716a.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b8395bd7e1285b90b36516b0f4eaf57161050ba36716a.jpg?mw=700" alt="reg-cli レポートのトップ" /></a></p> <p><code>reg-cli</code> のレポートのトップ。キャプチャのうちテストを通らなかったものが上部に、通ったものは下部に分類されて並べられています。</p> <p><a href="https://crieit.now.sh/upload_images/70a0a871c47de51ae4439ec6a769c4fa61050bacf3c2e.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/70a0a871c47de51ae4439ec6a769c4fa61050bacf3c2e.jpg?mw=700" alt="テストOKの例" /></a></p> <p>テストが通った例。トップページです。</p> <p><a href="https://crieit.now.sh/upload_images/d2b7e96fc061ad990c86cbdb10dcdbd961050bb5236f1.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d2b7e96fc061ad990c86cbdb10dcdbd961050bb5236f1.jpg?mw=700" alt="テストNGの例" /></a></p> <p>テストNGの例。なんだかディレクトリ構造のツリー構造がずれているようです。左右にバーをスライドさせて比較を行うことができる他、差分を赤色で表示させるモードや、 <code>ref</code> のみの表示、 <code>test</code> のみの表示、と切り替えることもできます。ここも使いやすそうだとおもった点です。</p> <p>これで、自分なりにビジュアルリグレッションテストができる環境が整備できた気がします。</p> <h2 id="余談"><a href="#%E4%BD%99%E8%AB%87">余談</a></h2> <p>もはや恒例となりましたが、名前について。今回は「Cypres bij Sterrennacht」。オランダ語です。日本語訳すると「糸杉と星の見える道」。ゴッホの最晩年の作品ですね。名前は Cypress をツールとして使用しているから、というシンプルな理由です。</p> <p>ちなみに自前の補助スクリプトの名前も</p> <ul> <li><code>cafe_bij_nacht-before.spec.js</code>: Cafe bij nacht →「夜のカフェ」</li> <li><code>cafeterras_bij_nacht-after.spec.js</code>: Cafeterras bij nacht →「夜のカフェテラス」</li> <li><code>sterrennacht.js</code>: Sterrennacht →「星月夜」</li> <li><code>zonnebloemen.js</code>: Zonnebloemen →「ひまわり」</li> </ul> <p>でいずれもゴッホの作品の名前から採りました。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <h3 id="Cypress"><a href="#Cypress">Cypress</a></h3> <h4 id="きっかけ"><a href="#%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91">きっかけ</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://future-architect.github.io/articles/20210428a/">Cypress入門~初心者でも簡単にE2Eテストが作れる~ | フューチャー技術ブログ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://future-architect.github.io/articles/20210428d/">Cypress - 書きやすいテストの秘密と独自コマンドの実装 | フューチャー技術ブログ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/okitan/items/b44882e28006c1be32b7">cypressを触ってみた - Qiita</a></li> </ul> <h4 id="ベース"><a href="#%E3%83%99%E3%83%BC%E3%82%B9">ベース</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/yodyman/items/3a2fbb106eaaca3aca47">Cypress + REG CLI ではじめたビジュアルリグレッションテスト - Qiita</a></li> </ul> <h4 id="Docs"><a href="#Docs">Docs</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/guides/overview/why-cypress">Why Cypress? | Cypress Documentation</a></li> </ul> <h4 id="前回実施時のスクリーンショットの破棄 (trashAssetsBeforeRuns)"><a href="#%E5%89%8D%E5%9B%9E%E5%AE%9F%E6%96%BD%E6%99%82%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%E3%81%AE%E7%A0%B4%E6%A3%84+%28trashAssetsBeforeRuns%29">前回実施時のスクリーンショットの破棄 (trashAssetsBeforeRuns)</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/55098694/how-to-delete-all-screenshots-before-a-test-run-while-doing-a-cypress-visual-reg">How to delete all screenshots before a test run while doing a cypress visual regression - Stack Overflow</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://uga-box.hatenablog.com/entry/2020/08/04/000000">【Cypress】Screenshotの設定 - UGA Boxxx</a></li> </ul> <h4 id="スクロール"><a href="#%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB">スクロール</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/api/commands/scrollto#Syntax">scrollTo | Cypress Documentation</a></li> </ul> <h4 id="要素を隠す"><a href="#%E8%A6%81%E7%B4%A0%E3%82%92%E9%9A%A0%E3%81%99">要素を隠す</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/api/cypress-api/screenshot-api#Change-the-DOM-using-onBeforeScreenshot-and-onAfterScreenshot">Cypress.Screenshot | Cypress Documentation</a></li> </ul> <p><code>.hide()</code></p> <h4 id="find の戻り値"><a href="#find+%E3%81%AE%E6%88%BB%E3%82%8A%E5%80%A4">find の戻り値</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/api/commands/find#Command-Log">find | Cypress Documentation</a></li> </ul> <h4 id="pluginsスクリプト のイベント"><a href="#plugins%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88+%E3%81%AE%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88">pluginsスクリプト のイベント</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/guides/tooling/plugins-guide#Run-Lifecycle">Plugins | Cypress Documentation</a></li> </ul> <h4 id="config"><a href="#config">config</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/guides/references/configuration#Command-Line">Configuration | Cypress Documentation</a></li> </ul> <h4 id="未使用"><a href="#%E6%9C%AA%E4%BD%BF%E7%94%A8">未使用</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/motodimago/items/9fb010026cb14ccab2b7">Cypressのheadlessモードでlogを表示させる - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/cypress-io/cypress/issues/186">Ability to add a message to stdout during cypress run ・ Issue #186 ・ cypress-io/cypress ・ GitHub</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/52070262/cypress-pipe-console-log-and-command-log-to-output">e2e testing - Cypress pipe console.log and command log to output - Stack Overflow</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.cypress.io/api/cypress-api/config#Notes">Cypress.config | Cypress Documentation</a> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/jnuank/items/25a1664ad186c9fb34e8">Vuetifyで作った画面のテストをCypressで自動化しようとした時に調べたことや、つまづいたこと - Qiita</a></li> </ul></li> </ul> <h3 id="reg-cli"><a href="#reg-cli">reg-cli</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/reg-viz/reg-cli">reg-viz/reg-cli: ?? Visual regression test tool.</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/17539 2021-07-25T21:32:29+09:00 2021-07-25T21:32:59+09:00 https://crieit.net/posts/for-await-Promise-all for文の中でawaitして返り値使う直列処理を Promise.all() で並列処理 <p>何か日本語の不自由なタイトルだが、実際僕の思った疑問をそのまま文章にするとこんな感じだった、というか実際こういう間抜けな文章で検索していたので、このままいくことにする。</p> <p>JavaScriptでfor文の中でawaitして直列でやっている処理を、Promise.all()を使って並列にやりたい、ということだ。const answers = Promise.all( …. ) と書きたい、という話。さらっと検索すると返り値を使うサンプル。</p> <h2 id="やりたいこと"><a href="#%E3%82%84%E3%82%8A%E3%81%9F%E3%81%84%E3%81%93%E3%81%A8">やりたいこと</a></h2> <p>論よりコードということで、以下のようなfor文があったとする。</p> <pre><code>function triangle (base, height) { return new Promise(resolve => { setTimeout(() => { const area = base * height / 2; console.log(area); resolve(area); }, 3000); }); } (async () => { let areas = [] for (const i of [1, 2, 3, 4, 5]) { const area = await triangle(i, i); areas.push(area); } console.log(areas); })(); </code></pre> <p>底辺と高さが1, 2, 3, 4, 5の三角形について面積を求めて、最後に出力している。直列でやっているため、3秒ずつ結果が順番に表示され、15秒後にすべての答えが表示される。</p> <p>これらの処理は独立であるので、並列で書けるはずだ。</p> <h2 id="並列化"><a href="#%E4%B8%A6%E5%88%97%E5%8C%96">並列化</a></h2> <p>これを並列化すると、以下のようになる。</p> <pre><code>(async () => { const areas = await Promise.all([1, 2, 3, 4, 5].map(async (element) => { return await triangle(element, element); })); console.log(areas); })(); </code></pre> <p>実行してみるとわかるが、3秒ですべての処理の答えが出るので、並列で処理してその結果を利用できることがわかる。</p> <p>2つの処理を同時に走らせてみると、以下のように出力される。</p> <pre><code>0.5 0.5 2 4.5 8 12.5 [ 0.5, 2, 4.5, 8, 12.5 ] 2 4.5 8 12.5 [ 0.5, 2, 4.5, 8, 12.5 ] </code></pre> <p>まず並列処理の回答がすべて表示されて、その後逐次直列の処理の結果が出ていることがわかる。</p> <p>以上。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <p>ありがとうございました。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://tomokazu-kozuma.com/how-to-use-async-await-promise-all-effectively-in-loop-processing-of-for-statement/">for文、forEach文のループ処理でasync/await、Promise.allを効果的に使う方法 – ブロックチェーンエンジニアとして生きる</a></li> </ul> tama tag:crieit.net,2005:PublicArticle/17530 2021-07-22T00:14:34+09:00 2021-07-22T00:14:34+09:00 https://crieit.net/posts/JavaScript-60f839da9668b 次の○曜日 や 来週の○曜日 を取得する JavaScript <p>JavaScript の標準組込みオブジェクトは、日時関連の処理が非常に貧弱だ。</p> <p>次の水曜日は何月何日? とか、 来週の月曜日は何月何日? といった値を取得しようと思ったら、タイムゾーン周りの処理と合わさって、思いのほか面倒だった。</p> <p>とりあえず、動くコードができたので、記事にして紹介する。</p> <pre><code class="js">/** * 「次の○曜日」を取得する * @param {Date} date 元となる日時 * @param {number} dayOfWeek 曜日を示す 0-6 の値。 Date.prototype.getDay() と同様、 0 は日曜日を表す。 * @param <span>{</span><span>{</span>?includeToday: boolean<span>}</span><span>}</span> option includeToday: 本日を含めるかどうかを指定する。省略時は false。 * @returns {Date} */ function getNextDayOfWeek(date, dayOfWeek, option) { option = option || {}; const includeToday = 'includeToday' in option ? option.includeToday : false; const retDate = new Date(date.valueOf()); retDate.setDate(retDate.getDate() + (includeToday ? 0 : 1)); retDate.setDate(retDate.getDate() + (7 - retDate.getDay() + dayOfWeek) % 7); return retDate; } // 実行例 console.log( [19,20,21,22,23,24,25,26,27,28,29] .flatMap(i => [new Date(`2021-07-${i}T00:00:00.000`), new Date(`2021-07-${i}T23:59:59.999`)]) .map(d => `${d.toLocaleString('en-CA')} => ${getNextDayOfWeek(d, 3 /* Wednesday */, null).toLocaleString('en-CA')}`) ); </code></pre> <pre><code class="js">/** * 「来週の○曜日」を取得する * @param {Date} date 元となる日時 * @param {number} dayOfWeek 曜日を示す 0-6 の値。 Date.prototype.getDay() と同様、 0 は日曜日を表す。 * @param <span>{</span><span>{</span>?weekStartDay: number<span>}</span><span>}</span> option weekStartDay: 週の始まりの曜日を指定する。省略時は 0 (日曜)。 * @returns {Date} */ function getDayOfNextWeek(date, dayOfWeek, option) { option = option || {}; const weekStartDay = 'weekStartDay' in option ? option.weekStartDay : 0; const retDate = new Date(date.valueOf()); retDate.setDate(retDate.getDate() + 1); retDate.setDate(retDate.getDate() + (7 - retDate.getDay() + weekStartDay) % 7 + (7 + dayOfWeek - weekStartDay) % 7); return retDate; } // 実行例 console.log( [19,20,21,22,23,24,25,26,27,28,29] .flatMap(i => [new Date(`2021-07-${i}T00:00:00.000`), new Date(`2021-07-${i}T23:59:59.999`)]) .map(d => `${d.toLocaleString('en-CA')} => ${getDayOfNextWeek(d, 5 /* Friday */, {weekStartDay: 1 /* Monday */}).toLocaleString('en-CA')}`) ); </code></pre> <p>引数などの使い方は、 JSDoc コメントを参照してのこと。</p> advanceboy tag:crieit.net,2005:PublicArticle/17503 2021-07-13T02:44:33+09:00 2021-07-13T02:44:33+09:00 https://crieit.net/posts/8dbd7e242deb435cd79ca8fdedbdfb4e 100万円失いながらハッキングを乗り越え誰でも1分で切り抜きを作れるサービスを公開するまでの失敗と学び <p>先日、<strong>誰でも最短1分でYouTubeの切り抜きを作れる</strong>ウェブサービスを公開しました。</p> <p>私はプログラミングの勉強を始めて1年半の初学者ですが、個人開発でサービスを公開するまでに、数多くの失敗と苦労をしてきました(そして今もしてます笑)。後ほど詳しく書きますが、以下のような経験をしました。</p> <ol> <li>ハッキングを受けデータを盗まれる</li> <li>α版をリリースするも作り直しを決意する</li> <li>巻き返しのため海外フリーランサーを雇うも無駄金となる</li> <li>公開前に本家が同じ機能を発表し諦めかける</li> <li>β版をリリースするも使われない</li> </ol> <p>同じようにプログラミングの勉強をし始めたばかりの方や、個人開発でいつかはサービスを公開したいと考えている方の「転ばぬ先の杖」として、私の経験が役に立てばと思っております。</p> <h1 id="開発したサービス"><a href="#%E9%96%8B%E7%99%BA%E3%81%97%E3%81%9F%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9">開発したサービス</a></h1> <p>YouTubeの公式APIを利用してウェブ上で切り抜きを作成・紹介できるウェブサービス「<strong>YouClip</strong>」を開発しました。</p> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/N801yLTnhbo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <p>ここから実際に触れます→https://youclip.app</p> <p>私自身が所属するスポーツチームの動画をYouTubeにアップすることが多くあり、試合やプレーの振り返りをするのにあったら便利だなと思ったのが着想のきっかけです。考えてみると、これはスポーツのプレー分析以外にも色々と用途があるのではないかと思い、プログラミングの学習も兼ねて一般向けのサービスとして開発を始めました。</p> <h1 id="今までの失敗と学び"><a href="#%E4%BB%8A%E3%81%BE%E3%81%A7%E3%81%AE%E5%A4%B1%E6%95%97%E3%81%A8%E5%AD%A6%E3%81%B3">今までの失敗と学び</a></h1> <h3 id="1.ハッキングを受けデータを盗まれる"><a href="#%EF%BC%91%EF%BC%8E%E3%83%8F%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0%E3%82%92%E5%8F%97%E3%81%91%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E7%9B%97%E3%81%BE%E3%82%8C%E3%82%8B">1.ハッキングを受けデータを盗まれる</a></h3> <p>開発を始めて間もないある日、いつものように本番サイトを開こうとするとエラーでページが表示されません。おかしいなと思いサーバ上のDBを確認すると、あったはずのテーブルが全て消えており、見慣れない<strong>WARNING</strong>と言う名のテーブルが1つだけありました。そこにはデータが1つだけ入っており、「データを復元して欲しければビットコインを振込め」との脅迫文が書かれていました。<br /> <img width="100%" alt="YouClip demo" title="YouClip demo" src="https://youclip-storage.s3.ap-northeast-1.amazonaws.com/thumbs/warning.png"><br /> (テーブルに入っていた全文)</p> <pre><code>To recover your lost databases and avoid leaking it: visit xxxx and enter your unique token xxxx and pay the required amount of Bitcoin to get it back. Databases that we have: xxxx, xxxx. Your databases are downloaded and backed up on our servers. If we dont receive your payment in the next 9 Days, we will sell your database to the highest bidder or use them otherwise. </code></pre> <blockquote> <p>(日本語訳)<br /> 失ったデータを復元し漏洩したくなければ、xxxxのサイトを訪れ、記載のトークンを入力した上で指定された金額のビットコインを支払え。お前のデータベースはダウンロードし、我々のサーバにバックアップしてある。9日以内に支払いがなければ、誰か高く買ってくれる人に売るか、別の方法で利用するだろう。</p> </blockquote> <p>そこではじめて何者かが<strong>DBをハッキングし、データを人質にビットコインを身代金として要求している</strong>のだと分かりました。幸いまだ開発初期で、ユーザーは身内と知り合い数人しかいなく、個人情報もメールアドレスくらいだったので、彼らにはお詫びをしてデータは諦めることにしました。まさかサーバに上げてすぐにハッキングされるとは思ってもおらず、衝撃でした。</p> <p>原因は、あまり深く考えずサーバにphpMyAdminを入れていたのですが、<strong>アカウント名やパスワードが単純</strong>だったため<strong>ブルートフォースアタック</strong>(総当り攻撃)でハッキングされたのだと思います(アホすぎるorz)。セキュリティに無知だった自分が悪いのですが、これが正式なリリース後だったら・・とゾッとしました。リリースする際には十二分にセキュリティ対策をしようと、苦い教訓となりました。(当然現在はphpMyAdminも入れてません)</p> <h3 id="2.α版をリリースするも作り直しを決意する"><a href="#%EF%BC%92%EF%BC%8E%CE%B1%E7%89%88%E3%82%92%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%99%E3%82%8B%E3%82%82%E4%BD%9C%E3%82%8A%E7%9B%B4%E3%81%97%E3%82%92%E6%B1%BA%E6%84%8F%E3%81%99%E3%82%8B">2.α版をリリースするも作り直しを決意する</a></h3> <p>当初作ろうと思っていた機能とデザインを一通り実装し終えたので、α版として身近な友人や親戚に使ってもらうことにしました。Adobe XDでUI案を作った段階で友人などからフィードバックはもらっていたし、自分でも使いやすく作ったつもりだったので、それほど大きな問題はないだろうと思ってました。しかし、いざ使ってもらうと<strong>肝心の切り抜き機能が使いづらい</strong>と感じる人が多いことが分かりました。そのため、<strong>切り抜き機能のフローやUIを抜本的に見直す</strong>ことを決意し、その影響はサービス全体にも及んで結果として大改修をする羽目になりました。</p> <p>Adobe XDでUI案を作った段階で友人からフィードバックをもらっていたのですが、それでも今回の問題に気付けなかったのは、2つ陥りがちな罠にはまっていたと思います。</p> <ul> <li>画面デザインを見せて意見を聞くだけで、<strong>ユーザーが操作しているところを観察しなかった</strong></li> <li>元からサービスの内容を知っている友人に意見を聞くだけで、<strong>完全に初見の人からフィードバックをもらわなかった</strong></li> </ul> <p>やはり実際に<strong>操作するのを見て初めて気が付く</strong>ことが多くあり、XDの画面デザインを見せるだけでなく、<strong>プロトタイプ機能</strong>を使って模擬的に操作してもらうべきでした。また、サービスの前提知識がある人やITリテラシーが高い人だと、多少分かりにくくても使いこなせてしまいます。実際にこれから使うターゲットユーザーに合わせ、<strong>初見でITリテラシーもそこまで高くない人</strong>からもフィードバックをもらうべきでした</p> <h3 id="3.巻き返しのため海外フリーランサーを雇うも無駄金となる"><a href="#%EF%BC%93%EF%BC%8E%E5%B7%BB%E3%81%8D%E8%BF%94%E3%81%97%E3%81%AE%E3%81%9F%E3%82%81%E6%B5%B7%E5%A4%96%E3%83%95%E3%83%AA%E3%83%BC%E3%83%A9%E3%83%B3%E3%82%B5%E3%83%BC%E3%82%92%E9%9B%87%E3%81%86%E3%82%82%E7%84%A1%E9%A7%84%E9%87%91%E3%81%A8%E3%81%AA%E3%82%8B">3.巻き返しのため海外フリーランサーを雇うも無駄金となる</a></h3> <h4 id="3−1.Crowdworks経由で中国人のエンジニアと働く"><a href="#%EF%BC%93%E2%88%92%EF%BC%91%EF%BC%8ECrowdworks%E7%B5%8C%E7%94%B1%E3%81%A7%E4%B8%AD%E5%9B%BD%E4%BA%BA%E3%81%AE%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E5%83%8D%E3%81%8F">3−1.Crowdworks経由で中国人のエンジニアと働く</a></h4> <p>大改修により、当初の予定よりだいぶリリース時期が遅れることが見込まれたため、巻き返しに<strong>スポットで外部のエンジニアを雇えないか</strong>考えました。知り合いをはじめ、Twitterや、<a target="_blank" rel="nofollow noopener" href="https://crowdworks.jp/">クラウドワークス</a>、<a target="_blank" rel="nofollow noopener" href="https://www.lancers.jp/">ランサーズ</a>などのクラウドソーシングサイトを利用して、フリーランスエンジニアの方々に連絡してみましたが、いいなと思う人はやはり時給にすると5,000円以上といった感じでした。<strong>自腹を切る上に、どれぐらい開発に時間がかかるかも読み切れない</strong>中で、言い方は悪いですが少しでもコストパフォーマンスの良さそうな人を必死に探しました。</p> <p>そんな中、<a target="_blank" rel="nofollow noopener" href="https://crowdworks.jp/">クラウドワークス</a>に登録していたエンジニアの1人が、技術スタックや経験、公開しているポートフォリオの観点からも良さそうで、かつ時給3,000円程度のオファーだったので、その人にお願いしてみることにしました。やりとりしている段階で気が付きましたが、登録してある情報は日本語でしたが実際には彼は<strong>中国人</strong>で、英語と中国語しか使えないようでした。私自身は留学経験もあり、英語でやりとりするのに抵抗はなかったのでそのまま彼と契約することにしました。</p> <p>彼とは作業時間に応じて支払いをする形(プロジェクト形式)で、途中休暇を挟みながらも足掛け2ヶ月程度リモートで一緒に働いたのですが、<strong>完成前に契約を打ち切りました</strong>。今も使っている有用なライブラリを紹介してくれたり、技術力はある程度期待通りだったのですが、次第に<strong>胡散臭いところや、面倒な交渉が多く発生するようになった</strong>のが原因です。</p> <p>例えば、タスク量はそれほど変わっていないはずなのに、<strong>後半になるにつれ作業時間がなぜか右肩上がり</strong>で増えていました。また、兄弟がコロナにかかって重症で大変なので<strong>ボーナスをくれないか</strong>と交渉されたり、契約の終わりの方には次のクライアントを見つけるのに海外のフリーランスサイトに登録したいが出来ないので、<strong>アカウントを貸してくれないか</strong>とお願いされたりしました。このようなことが積み重なって、次第に信用できなくなっていきました。</p> <h4 id="3−2.海外のクラウドソーシングサイトでインド人・ウクライナ人・スペイン人と働く"><a href="#%EF%BC%93%E2%88%92%EF%BC%92%EF%BC%8E%E6%B5%B7%E5%A4%96%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E3%82%BD%E3%83%BC%E3%82%B7%E3%83%B3%E3%82%B0%E3%82%B5%E3%82%A4%E3%83%88%E3%81%A7%E3%82%A4%E3%83%B3%E3%83%89%E4%BA%BA%E3%83%BB%E3%82%A6%E3%82%AF%E3%83%A9%E3%82%A4%E3%83%8A%E4%BA%BA%E3%83%BB%E3%82%B9%E3%83%9A%E3%82%A4%E3%83%B3%E4%BA%BA%E3%81%A8%E5%83%8D%E3%81%8F">3−2.海外のクラウドソーシングサイトでインド人・ウクライナ人・スペイン人と働く</a></h4> <p>ただ、<strong>作業自体はある程度スピードアップ</strong>したのは確かだったので、もう少し試してみようと、今度は直接海外のクラウドソーシングサイトを利用してみることにしました。前回の反省を活かして、数時間で出来る小さいタスクに対するコンペ方式で支払いを抑えつつ、何人か試してみていい人がいたら継続しようと考えました。</p> <p>海外のクラウドソーシングサイトで有名な<a target="_blank" rel="nofollow noopener" href="https://www.freelancer.com/">Freelancer.com</a>や<a target="_blank" rel="nofollow noopener" href="https://www.upwork.com/">Upwork</a>を使って、<strong>インド人、ウクライナ人、スペイン人</strong>と働いてみることにしました。確かに金額的には日本よりだいぶ低く抑えられましたが、<strong>お願いした仕様と全く違うもの</strong>を作ってきたり、その多くが自分でやり直さなければいけないことになりました。</p> <p>結局<strong>トータル100万円</strong>近く掛けましたが、果たしてその価値があったかと聞かれると「はい」とは素直に言えない感じになりました。日本のフリーランスの方と働いたことがないため一概には言えないものの、<strong>(中国・インド等の)海外エンジニアだからコスパ的に良いといったことはない</strong>というのが今回の感想です。</p> <h3 id="4.公開前に本家が同じ機能を発表し諦めかける"><a href="#%EF%BC%94%EF%BC%8E%E5%85%AC%E9%96%8B%E5%89%8D%E3%81%AB%E6%9C%AC%E5%AE%B6%E3%81%8C%E5%90%8C%E3%81%98%E6%A9%9F%E8%83%BD%E3%82%92%E7%99%BA%E8%A1%A8%E3%81%97%E8%AB%A6%E3%82%81%E3%81%8B%E3%81%91%E3%82%8B">4.公開前に本家が同じ機能を発表し諦めかける</a></h3> <p>改修も終えそろそろリリースできそうだなと考えていたある日、<strong>YouTube本家</strong>が動画の一部を切り取ってSNSでシェアできる<strong>クリップ</strong>と言う機能を実験的に米国で開始したと言う<a target="_blank" rel="nofollow noopener" href="https://japan.cnet.com/article/35165756/">ニュース</a>が流れてきました。</p> <p>本家が、名前もほぼそのまま、同等の機能を出してくるとは思ってもおらず、リリースしても本家に勝てるはずがないと諦めかけました。しかし少し冷静になって考えてみると、まだ実験的な位置付けで本当に追加されるかもわからないことに加え、そもそも<strong>本家は1つの場面しか切り抜けないが、YouClipは複数の場面を切り抜いて繋げられる</strong>ことなど、機能面でも違いがありました。</p> <p>そこで、この機能面の違いに目を向ければ、特定のユースケースでYouClipにも需要がまだあるのではないかと考え直し、そのユースケースに集中しようと考え直しました。具体的には、複数の場面を切り抜いて繋げる必要があるような、<strong>長時間のライブ配信</strong>がメインの<strong>VTuber動画</strong>や、<strong>ゲーム実況配信</strong>にサービスの主眼を置くことにしました。</p> <h3 id="5.β版をリリースするも使われない"><a href="#%EF%BC%95%EF%BC%8E%CE%B2%E7%89%88%E3%82%92%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%99%E3%82%8B%E3%82%82%E4%BD%BF%E3%82%8F%E3%82%8C%E3%81%AA%E3%81%84">5.β版をリリースするも使われない</a></h3> <p>そうやって何とかβ版のリリースに漕ぎ着け、知り合いやTwitter経由でユーザーを集め始めたのですが、一度はサイトに訪問してくれるものの、そのまま何もせずに帰ってしまう人がほとんどでした。Google Analyticsで見てみると、ホーム画面や再生画面の<strong>直帰率が70%近く</strong>あり、全く以て<strong>穴の開いたバケツ</strong>のような状態であることがわかりました。</p> <p>そこからは直接ユーザーに意見を聞いたり、Analyticsで取れる各種KPIや<a target="_blank" rel="nofollow noopener" href="https://clarity.microsoft.com/">Microsoft Clarity(※)</a>の画面レコーディングデータを見つつ、仮説を立てて離脱率が下がるよう改善を繰り返しました。</p> <p>例えば、再生画面の画面レコーディングを見ると、ミュート解除ボタンに気づかず、そのままミュート状態で見続けているユーザーが多くいることに気づきました。ミュート状態だと面白さは半減するため、<strong>ミュート解除ボタンをより目立つ位置と見た目に変える</strong>ことで、再生画面の離脱率を10pt%近く下げることができました。<br /> <img width="90%" alt="ミュート解除の改善" title="ミュート解除の改善" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/23090/e846cb3a-c2b6-011e-3540-1b60dd190bd0.png"></p> <p>まだまだ十分とは全く言えない状態ですが、このような小さな改善を繰り返すことで、バケツの穴を少しずつ塞いでいっています。</p> <blockquote> <p>(※)Microsoftの無料ヒートマップツール「Clarity」について<br /> Clarityは、ユーザーの行動を無料で把握できる分析ツールで、匿名化されたユーザーの画面操作記録を見ることが出来ます。</p> </blockquote> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/KAibwlJnx9Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <blockquote> <p>詳しくは以下のサイトなどを見てもらうと良いと思いますが、実際にユーザーがどんな風にサービスを使っているのかを知るのにとても役立ちます!<br /> https://sogyotecho.jp/clarity/</p> </blockquote> <h1 id="最後に"><a href="#%E6%9C%80%E5%BE%8C%E3%81%AB">最後に</a></h1> <p>このようにたくさん失敗をし、正直こんなことやって意味あるのか自問自答することもありましたが、長年やってみたいと思っていた個人開発でサービスを作れた事は楽しかったし良い経験となりました。</p> <p>こんなこと出来ないかと空想することから始まり、そこからどうやったら実現出来るか1つずつ考え・調べて・学びながら実装していく作業はとても楽しかったです。そうやって苦労して作ったサービスが誰かに使われているのを見るのはとても嬉しく、新しく投稿があると思わず全部いいねしてしまいます笑</p> <p>また、当然サービスをリリースしても知ってもらわなければ意味がないので、苦手なSNSの運用やマーケティングについても現在進行系で試行錯誤しながら学んでいます。(こちらも現在進行系で失敗?!しているので、いつか振り返りたい)</p> <p>以上が個人開発でサービスを公開するまでの私の失敗と学びです。<br /> 何かしら個人開発をしている人、これからしたい人の参考になれば嬉しいです!</p> Kendai