20220117のJavaScriptに関する記事は11件です。

Vueで要素外クリック検出

例えばメニューを実装する際に、メニュー外をクリックしたら閉じるようにしたいときが割とあるため備忘録として。 Nodeのcontainsを使い、クリックされた要素がメニュー要素の子孫に含まれるかどうかを判定しています。 vueでwindowやdocumentにイベントリスナーを追加するとコンポーネントがunmountされてもイベントリスナーが残り続けるため、必ずunmount時にremoveしましょう。 <script setup lang="ts" /> import { ref, onMounted, onBeforeUnmount } from 'vue' const element = ref<HTMLDivElement | null>(null) // 対象の要素 const clickOutside = (e: MouseEvent) => { // [対象の要素]が[クリックされた要素]を含まない場合 if (e.target instanceof Node && !element.value?.contains(e.target)) { // 何らかの処理 } } // windowにセットしたイベントはremoveするのを忘れずに onMounted(() => { addEventListener('click', clickOutside) }) onBeforeUnmount(() => { removeEventListener('click', clickOutside) }) </script> <template> // 対象の要素(メニューとか) <div ref="element" > ... </div> </template>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】外部リンクに飛ぶ方法【Javascript】

外部のページに飛びたい ReactでSPAアプリ作成時にReactRouter外の外部のページに飛びたい。 しかし、useHistoryやLinkでは飛べない。 そういう場面に直面して解決したのでその方法をここに記します。 実装方法 リンクで飛ぶ //現在のタブで開く <a href="https://www.google.com/">Text</a> //新しいタブで開く <a target="_blank" href="https://www.google.com/">Text</a> 何らか処理後に飛ぶ export const Hoge () => { 現在のタブで開く function onClickHandleA () { window.location.replace("https://www.google.com/") } //新しいタブで開く function onClickHandleB () { window.open("https://www.google.com/", '_blank'); } return ( <div> <div onclick={onClickHandleA }> Text </div> <div onclick={onClickHandleB }> Text </div> </div> ) } 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

querySelectorはdocument以外のElementでも使える

HTML <body> <div class="first-element"> <div class="some-child">Hi!</div> </div> <div class="second-element"> <div class="some-child">Bye!</div> </div> </body> JS const $second = document.querySelector('.second-element'); const $child = $element.querySelector('.some-child'); console.log($child.innerText); // => Bye! Reactだと↓のように使えたりして便利(子要素のref使えというツッコミはなしで ref.current.querySelector('.some-child')
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Chrome】ServiceWorkerを今度こそ決定的かつ完全に消去する

前、Chromeを使うなら、必ずServiceWorkersを無効化しようという記事を書いたのですが、あの方法はてんで全然ダメダメでした。 chrome://serviceworker-internalsには無用なServiceWorkerが増え続け、chrome://flags/からはServiceWorkerの削除設定が削除されました。 そんなわけで、今度こそServiceWorkerを決定的かつ完全に消し去りたいと思います。 といっても自力でどうこうしたわけではなく、全面的に他人の力を借りただけですが。 Reject Service Worker Reject Service Workerをインストールする。 おわり。 かんたん! 作者はtoshiさんです。 ありがとうございます。 アドオンの技術的内容はServiceWorkerを無効化するという記事に書かれているのですが、ServiceWorkerを登録する関数を書き替えて登録できないようにしているみたいです。 なかなかのハック感。 既存のServiceWorkerを削除 Reject Service Workerは、今後のServiceWorkerの新規登録こそ止めてくれますが、それまでに登録されてしまったServiceWorkerはそのままであり消去されません。 削除する方法はふたつ。 まずは、Reject Service Workerが有効な状態で、ServiceWorkerが登録されたサイトを再度訪問すれば、そのタイミングで消えます。 もうひとつは、URLにchrome://serviceworker-internalsと入力し、ひたすらUnregisterを連打することです。 もしくはコメントで、一括削除する方法を @negabaro さんが紹介してくれていました。 $$('.unregister').forEach(b => b.click()) ホワイトリスト ホワイトリストが用意されています。 このサイトだけはServiceWorkerを許可したい、といった場合は以下の手順で登録できます。 ・許可したいサイト上で拡張機能からReject Service Workerを選択する。 ・するとAdd to Whitelistというボタンが出るので選択する。 とても簡単ですね。 まあ、そもそもデフォルトがホワイトリストであるべきで、勝手にインストールしてくる現状が異常なわけですが。 ソースコードを確認 せっかくServiceWorkerを消し去ることに成功したとはいえ、実はアドオンそものがマルウェアでしたみたいなことになったら台無しですね。 そんなわけでReject Service Workerが安全かを、実際にソースコードを見て確認してみましょう。 アドオンの位置は%LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions\falajmifjcihbmlokgomiklbfmgmnopdです。 Defaultはプロファイル名なので、場合によって異なる可能性があります。 幾つかファイルがありますが、中身はただのJavaScriptなので普通に読めます。 ソースコードはコメントを用意してくれているのでとても読みやすいです。 ソースコードを見るかぎりでは、何か怪しいことをやっていたり、外部と通信しているような素振りは全くありませんでした。 そもそも作者自身がアドオンのソースコードを確認する方法を提供していますからね。 これで変なことをやっていたら自爆でしかない。 ということで、このアドオンは所有者が変わらないかぎりきっと安全であり続けるでしょう。 まとめ Reject Service Worker導入前後で、インターネット体験には何一つ変化がありません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Microsoft TeamsのWebhook URLにNode.jsから情報を送ってみる ~その2~

ただのテキスト以外も送ってみたい 前回の記事の続きで、ちょっとリッチな情報を送ってみます。 前回は最もシンプルなテキスト送信を試していました。 WebhookのURLの取得は前回記事参照です。 こちらのドキュメントを参考に書いてみました。 コピペ用コード こんな感じです。 'use strict' const axios = require('axios'); const URL = `https://xxxxxxxxxxxxxxxxxxxxxxx`; axios.post(URL, { "@type": "MessageCard", "@context": "https://hogehoge.com/hogehoge.png", "themeColor": "0076D7", "summary": "ディスカッション", "sections": [{ "activityTitle": "Larry Bryant created a new task", "activitySubtitle": "On Project Tango", "activityImage": "https://hogehoge.com/xxxxx.png", "facts": [{ "name": "投稿者", "value": "うこ" }, { "name": "URL", "value": "https://hogehoge.com/xxxx/yyy" }, { "name": "Status", "value": "Not started" }], "markdown": true }] }) .then(res => console.log(res.data)); こんな処理を実行すると冒頭に載せた画像のような通知がTeamsに投稿されます。 activityImageが表示されない activityImageに画像を指定すればアイコンっぽいのが表示されるようなことがドキュメントに書いてますが、何度か試してもうまくいきません。 エラーが出るわけでもないので何かの読み込みが追いついてないのかなんなのか...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptでボタン押した時にローディング画像表示(CSS変更するのも面倒くさい人向け)

はじめに  完全に後付でCSSとか使わずに(面倒くさいから)、いい感じにhtmlに画像表示するのに、あっちこっち検索したので備忘録的に作成。  基本的にCSS使ったいい感じのサンプルたくさんあるので、そっち使った方がいいです。  CSS変更できないけどjavascriptは変更できる環境の人向け(そんな人居るのか?)。  ローディングのgif画像は各自用意してください。 ソースコード javascriptの一番下にでもコピペしてください。 jquery使ってます。 $(function() { var $btnLoading = $('.btn_loading'); $btnLoading.on('click' ,function(){ setTimeout(function(){ $('body').prepend('<div class="loading" style="display:flex; width:100vw; height:100vh; background-color: rgba(0,0,0,0.6); position:absolute; align-items:center; justify-content:center;"><img src="loading.gif"></div>'); },100); }); }); ローディング画像挟みたいボタンに"btn_loading"のクラスを追加。 <input type="submit" value="OK" class="btn_loading"> 簡単解説 <div class="ajax-loader" style="display:flex; width:100vw; height:100vh; background-color:rgba(0,0,0,0.6); position:absolute; align-items:center; justify-content:center;"> <img src="loading.gif"> </div> bodyの一番上にウィンドウ全体透過度60%の黒の重なるdiv要素を追加してます。 あとgif画像を "display:flex; align-items:center; justify-content:center;" で上下左右の中央に表示するようにしてます。(これが地味にハマった)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

canvasを使いお絵描きアプリを作る。(各機能ざっくり解説)

開発を進めていく上で意外とcanvasの知識が無かったので復習・備忘録も兼ねてcanvasの説明をしていきます。 コレちゃうねん何言ってんだこいつって点があれば教えてくださいまし。 目次 1.canvasとは 2.HTML側のコーディング 3.JS側のコーディング 4.各種ペンツール 5.プレビュー機能 6.クリア機能 7.ダウンロード機能 8.ソースコード全容 1. canvasとは いつもお世話になっているmozillaさんの引用となります。 HTML の 要素 と Canvas スクリプティング API や WebGL API を使用して、グラフィックやアニメーションを描画することができます。 簡単に言っちゃえばcanvasの中にアニメーションやグラフィックを描画することが可能です。 なのでマウスの動作を利用して署名動作を行う際に利用したり、WEBでお絵描きができます。 ※今回はスマホは非対応になっています。(対スマホコーデディングがめんどくさかったので) 完成したもの こちらになります。 グレーの背景にマウスドラッグするとペンの描画が始まって、絵やサインを書いてくれます。 下のPencil Color,Size,Opacityでペンの色、太さ、透過度を調整可能です。 また、ダウンロード、プレビュー、クリアボタンなどの各種機能を作ってみました。 これらの機能を順を追って解説していきます。 2. HTML側のコーディング HTML側のコーディングは至ってシンプルです。 今回はCSSのフレームワークにBuluma.cssを採用しています。 デザインがシンプルかつカッコよくてコーディングが短くて済むので、簡単なサンプルを作る際に重宝しています。 index.html <div class="columns"> <div class="column is-half"> <canvas id="canvasArea"></canvas> </div> <!-- 省略 --> </div> 本来canvasは高さ、幅を指定しないといけませんが今回はjs側に指定する挙動にしました。 共通クラスにする設計にした為です。 なので最小構成はこんな感じでイケます。 3. JS側のコーディング JS側は少し量が増えます。 といってもさほど大した量ではありません。 取得した要素に対してnullチェックを行っているのはカスタマイズ性を意識しています。 previewとかダウンロードいらないよ。ってのはあるかもしれないのでその為です。 コンストラクタ コンストラクタ部分はcanvasのid、幅、高さ、各種セレクターを指定します。 その上で初期化処理を実施します。 painter.js class Painter { constructor(selectorId, width, height, pencilSelector = { colorPencil: '#pencilColor', colorPalette: '.color-palette', pencilSize: '#pencilSize', pencilOpacity: '#pencilOpacity', clearButton: '#clearButton', downloadButton: '#downloadButton', previewButton: '#previewButton', previewArea: '#preview', }) { this.selectorId = selectorId; this.width = width; this.height = height; this.pencilSelector = pencilSelector this.x = null; this.y = null; this.init(); } /** * initialize function. */ init = () => { this.element = document.getElementById(this.selectorId); this.clearButton = document.querySelector(this.pencilSelector.clearButton); this.downloadButton = document.querySelector(this.pencilSelector.downloadButton); this.previewButton = document.querySelector(this.pencilSelector.previewButton); this.previewArea = document.querySelector(this.pencilSelector.previewArea); if (this.element == null) { throw Error('[painter.js] Selector is not found. Please specify the id.'); } if (this.element.tagName !== 'CANVAS') { throw this.error(`${this.selectorId} is not canvas`); } this.element.width = this.width; this.element.height = this.height; this.element.addEventListener('mousemove', this.onMouseMove); this.element.addEventListener('mousedown', this.onMouseDown); this.element.addEventListener('mouseout', this.drawFinish); this.element.addEventListener('mouseup', this.drawFinish); if (this.clearButton != null) { this.clearButton.addEventListener('click', this.clearCanvas); } if (this.downloadButton != null) { this.downloadButton.addEventListener('click', this.download); } if (this.previewButton != null) { this.previewButton.addEventListener('click', this.preview); } if (this.previewArea != null) { this.previewArea.src = './image/no-preview.jpg'; this.previewArea.width = this.width; this.previewArea.height = this.height; } this.context = this.element.getContext('2d'); this.setCanvasStyle(); // init pencil setting. this.penSize = 3; this.penColor = '#000000'; this.penOpacity = 1; this.initColorPencilElements(); } /** * set canvas style */ setCanvasStyle = () => { this.element.style.border = '1px solid #778899'; this.context.beginPath(); this.context.fillStyle = "#f5f5f5"; this.context.fillRect(0, 0, this.width, this.height); } // 省略 } const painter = new Painter('canvasArea', 564, 407); 解説:constractor 特にいう事がありません。 渡された引数を元に変数の初期化したり・・・などなど。 解説:init イベントハンドラの紐づけや引数のチェックを行います。 またhtmlのcanvas側に指定しなかった幅、高さの設定もこちらで実施します。 特記すべきところはこちらでしょうか。 painter.js this.element.addEventListener('mousemove', this.onMouseMove); this.element.addEventListener('mousedown', this.onMouseDown); this.element.addEventListener('mouseout', this.drawFinish); this.element.addEventListener('mouseup', this.drawFinish); ここでthis.element= canvasの要素に対してイベントハンドラを紐づけます。 スマホになるともう少しひと手間必要ですが、今回は省略します。。(めんどくさいのだ。。。) またこちらで呼び出されてるinitColorPencilElementsは後程解説します。 解説:setCanvasStyle canvasの初期設定をします。 またクリアボタン押下時にも呼び出される関数となります。 canvas要素のbeginPath()を呼び出してパスを開始します。 基本的にcanvasは任意の場所に点をおいて、繋げていく→線や図形や文字になる。といった概念です。 この辺りは説明がすんごくしんどいので公式を見てもらうと良いかもしれません。 その為こちらでは説明は割愛しますが、描画を開始する際に呼び出される関数といった形で覚えてください。 (分かりやすい説明があればコメントお待ちしています。) 描画処理 init()で設定されたイベントハンドラの中で動く関数の説明になります。 ここからが本題となります。 painter.js /** * Calculate the coordinates from the event. * @param {*} event */ calcCoordinate = (event) => { const rect = event.target.getBoundingClientRect(); const x = ~~(event.clientX - rect.left); const y = ~~(event.clientY - rect.top); return {x, y}; } /** * mouse down event * @param {*} event */ onMouseDown = (event) => { if (event.button !== 0) { return; } const coordinate = this.calcCoordinate(event); this.draw(coordinate); } /** * mouse move event * @param {*} event */ onMouseMove = (event) => { if (event.buttons !== 1) { return; } const coordinate = this.calcCoordinate(event); this.draw(coordinate); } /** * End of drawing process. */ drawFinish = () => { this.x = null; this.y = null; } /** * drawing process * @param {*} coordinate */ draw = (coordinate = {x: 0, y: 0}) => { const {x: toX, y: toY} = coordinate; this.context.beginPath(); this.context.globalAlpha = this.penOpacity; const fromX = this.x || toX; const fromY = this.y || toY; this.context.moveTo(fromX, fromY); this.context.lineTo(toX, toY); this.context.lineCap = 'round'; this.context.lineWidth = this.penSize; this.context.strokeStyle = this.penColor; this.context.stroke(); this.x = toX; this.y = toY; } 解説:onMouseDown マウス押下時に発火されるイベントです。 マウスの座標を計算し、描画処理を行う関数です。 見ての通り処理の詳細はcalcCoordinate()とdraw()に集約されています。 解説:onMouseMove mouseDownとほぼ同じです。 ですがmouseDown時はevent引数の状態が違うので差別化する必要があります。 こちらも処理の詳細はcalcCoordinate()とdraw()に集約されています。 解説:calcCoordinate イベント引数からマウスの座標を計算します。 単純にclientX, clientYだけの計算だとページがスクロールや拡大・縮小した際に座標の位置が狂います。 それを防ぐためgetBoundingClientRectにて要素に対するウィンドウ座標を出力してあげて、計算してあげる必要があります。 これによってスクロールした際に点が明後日の方向にいかないように制御することができます。 最終的にチルダ2つで小数点を省き絶対値のみの値にしてx,y座標を返します。(そこまで細かい描画はしない為) 解説:draw 引数はcalcCoordinateの戻りです。 先程軽く触れましたが描画にはパスの繋ぎが必要になりますので、fromX,toX, fromY, toYの4つの変数が重要になります。 painter.js // 座標の変数を展開して const {x: toX, y: toY} = coordinate; // ここで開始の座標を指定する。this.x, this.yがnullならばto === fromになるので値を代入 const fromX = this.x || toX; const fromY = this.y || toY; // 座標の開始位置まで移動 this.context.moveTo(fromX, fromY); // 座標の終了位置までパスをつなぐ this.context.lineTo(toX, toY); 上記コードでパスを繋ぎました。 実際の色を塗る処理はこちらになります。 painter.js this.context.lineCap = 'round'; // 丸形のペン(今回は固定にしました。) this.context.lineWidth = this.penSize; // ペンサイズを指定 this.context.strokeStyle = this.penColor; // ペンの色を指定 this.context.stroke(); // 描画する つたない説明になってしまいましたが、こんな感じです。 4. 各種ペンツール 無駄に拘ってしまったペンツールのご紹介。 init()で省略したペンのスタイルについてです。 painter.js /** * Initialize colored pencils */ initColorPencilElements = () => { const { colorPencil: color, colorPalette: palette, pencilSize: size, pencilOpacity: opacity } = this.pencilSelector; const colorPencil = document.querySelector(color); const colorPalette = document.querySelector(palette); const pencilSize = document.querySelector(size); const pencilOpacity = document.querySelector(opacity); if (colorPencil != null) { colorPencil.value = this.penColor; colorPencil.addEventListener('click', (ev) => { ev.target.type = 'color' }); colorPencil.addEventListener('blur', (ev) => { ev.target.type = 'text'; if (colorPalette != null) { colorPalette.style.backgroundColor = ev.target.value; } }); colorPencil.addEventListener('change', (ev) => { this.penColor = ev.target.value; }); } if (colorPalette != null) { colorPalette.style.backgroundColor = this.penColor; } if (pencilSize != null) { pencilSize.value = this.penSize; pencilSize.addEventListener('change', (ev) => { this.penSize = ev.target.value; }); } if (pencilOpacity != null) { pencilOpacity.value = this.penOpacity; pencilOpacity.addEventListener('change', ev => { this.penOpacity = ev.target.value; }); } } 解説:initColorPencilElements ペンのスタイルを定義します。それだけならまだいいのですが、、、 標準のinput type="color"が死ぬほどダサくてカスタマイズしました。 基本的にブラウザ標準のがダサすぎるんですよね・・・。 inputに色がビーって引っ張る辺りが相当ダサくて・・。 なのでこんな感じでクリックしたらcolorに変化して、blurしたらtextにするようにしました。 そしてchangeイベントで横のBOXの色が変化するようにしてみました。 これらの設定は一旦クラス変数に保持してあげて終わり。draw()時にその設定を利用するようにしています。 5. プレビュー機能 画像を一旦プレビューしたい時ってありますよね。 隣にあるから需要が少ないと思いますが、Google Lens等の画像検索したい時に便利です。 簡単ですね。imageタグの属性を変えてあげるだけです。 index.html <div class="column is-half preview-area"> <img id="preview" /> </div> painter.js /** * show preview */ preview = () => { this.previewArea.src = this.element.toDataURL(); // canvasの要素をdataURLに変換 // 高さ・幅調整 this.previewArea.width = this.width; this.previewArea.height = this.height; } 6. クリア機能 本来なら要素をクリアするだけで良いのですが、今回はcanvasなのでそうはいきません。 clearRect()で開始位置のx,y座標は0を指定し、終了位置は要素の幅、高さを指定しましょう。 そしてthis.setCanvasStyle();を再度呼び出してあげれば元の状態に戻ります。 painter.js /** * clear canvas */ clearCanvas = () => { this.context.clearRect(0, 0, this.element.width, this.element.height); this.setCanvasStyle(); } 7. ダウンロード機能 描画した画像をダウンロードします。 blob形式に変えて動的URLを生成してクリック動作を発火させます。 クライアント側ダウンロードと同じ挙動ですね。 painter.js /** * download image. */ download = () => { this.element.toBlob((blob) => { const url = URL.createObjectURL(blob); const aTag = document.createElement('a'); document.body.appendChild(aTag); aTag.download = 'drawImage.png'; aTag.href = url; aTag.click(); aTag.remove(); URL.revokeObjectURL(url); }); } 8. ソースコード全容 こちらが該当のソースになります。 https://github.com/kinachan/canvasSample 公開するときに色々と無駄なコードが残っていて焦りました。 もっと発信が出来るように頑張ろうとおもいました。  余談 今回はVanilla縛りだったので仕方ないのですがTypescriptで書いた方がもっときれいに書けるのに・・と思いました。 やっぱりTypescriptはつよい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

"use-shopping-cart"を使って、StripeとReactで簡単にECカート機能 + 決済機能を実装しよう

Stripeを利用することで、決済機能を簡単に実装することができます。 しかし通販機能を実装するためには、決済だけではなく買い物かご(カート)機能も実装する必要があります。 今回は、「use-shopping-cart」を利用して、Reactアプリで簡単にカート機能を実装する方法を紹介します。 use-shopping-cartとは use-shopping-cartは、Reactでショッピングカート機能を実装するために必要な機能を備えたHookを提供するライブラリです。 内部的にReduxを利用しており、「カート機能」と「Stripe Checkoutへの遷移」の2機能が利用できます。 use-shopping-cartでできること(一部) use-shopping-cartを利用することで、一般的なショッピングカートを実装するために必要なAPIを簡単に手に入れることができます。 商品をカートに追加する カートに追加した商品データや合計金額データを取得する 商品をカートから削除する カート内の商品の個数を変更する etc... use-shopping-cartを使ってカート機能を実装しよう サンプルアプリのセットアップ Create React AppやViteを利用してアプリをセットアップしましょう。 // CRA % npx create-react-app my-app // Vite % npm init vite ライブラリをインストールしよう useShoppingCartライブラリをインストールします。 この際Stripe.jsも一緒にインストールします。 ただしReact SDKは不要ですのでご注意ください。 % npm install @stripe/stripe-js use-shopping-cart CartProviderを設定しよう use-shopping-cartは内部的にReduxを利用しています。 そのため、アプリ全体をCardProviderの子要素にする必要があります。 import ReactDOM from 'react-dom' import { CartProvider } from 'use-shopping-cart' import App from './App' ReactDOM.render( <CartProvider mode="payment" cartMode="client-only" stripe={YOUR_STRIPE_API_KEY_GOES_HERE} successUrl="http://localhost:3000" cancelUrl="http://localhost:3000" currency="JPY" allowedCountries={['US', 'GB', 'JP']} billingAddressCollection={true} > <App /> </CartProvider>, document.getElementById('root') ) loadStripeやredirectToCheckoutなどで設定するパラメータをここで設定します。 Providerで設定することで、redirectToCheckoutを呼び出す際にリダイレクトURLなどの情報を省略することができます。 cartModeでStripe Checkoutの起動方法が変更可能です cartModeには、checkout-sessionとclient-onlyの2種類が設定できます。 client-onlyを利用した場合は、checkout.sessions.createAPIを利用せずに、クライアント側だけでStripe Checkoutへのリダイレクトを実施します。 APIでCheckoutのセッションを作成したい場合は、checkout-sessionを利用しましょう。 商品・料金データを設定しよう 商品データを作成します。簡単な方法はJSONまたはオブジェクトで設定することです。 price_idとpriceの2つが欠けていると、うまく動作しませんので注意しましょう。 const productData = [ { name: 'Bananas', price_id: 'price_xxxx', price: 400 }, { name: 'Tangerines', price_id: 'price_xxxx', price: 100 } ] 続いて設定した商品データをReactで表示させましょう。 import { formatCurrencyString } from 'use-shopping-cart' ... <ul> {productData.map(product => ( <li key={product.price_id}> {product.name} ({formatCurrencyString({ value: product.price, currency: 'JPY' })}) <br /> <button> Add 1 to Cart </button> </li> ))} </ul> formatCurrencyStringを利用することで、金額を指定した通貨に合わせた表示に変換することができます。 カート機能を実装する いよいよカート機能の実装に入ります。 カートに関するメソッドなどは、useShoppingCartフックを利用して取得します。 import { useShoppingCart } from 'use-shopping-cart' ... const { totalPrice, cartCount, addItem, cartDetails, ...args } = useShoppingCart() カートに商品を追加する機能を実装しよう まずは表示している商品をカートに追加するボタンを実装しましょう。 import { formatCurrencyString, useShoppingCart } from 'use-shopping-cart' const Shop = () => { const { addItem } = useShoppingCart() return ( <div> <ul> {productData.map(product => ( <li key={product.price_id}> {product.name} ({formatCurrencyString({ value: product.price, currency: 'JPY' })}) <br /> <button onClick={() => addItem(product)}> Add 1 to Cart </button> </li> ))} </ul> </div> ) } buttonのonClickイベントにaddItemを設定しました。 これで選択した商品をカートに追加完了です。 カートの中身を確認しよう 追加に成功しましたが、実際にカートの中に何が登録されているかがわかりません。 そこでカートの中身についても表示させてみましょう。 const Shop = () => { const { addItem, + cartDetails, } = useShoppingCart() return ( <div> ... <section> <h1>Cart</h1> <ul> {Object.entries(cartDetails).map(([priceId, cartItem]) => ( <li key={`cart-${priceId}`}> {cartItem.name} ({cartItem.formattedPrice} * {cartItem.quantity} = {cartItem.formattedValue})<br/> <button>Increase</button> <button>Decrease</button> <button>Cancel</button> </li> ))} </ul> </section> </div> ) } cartDetailsを使用することで、カートに追加された商品データを取得・表示することができます。 # カートの商品数を変更できるようにしよう カートに追加した商品の数を増減させたり、削除することもできます。 以下のサンプルを参考に、コードを更新しましょう。 const Shop = () => { const { addItem,  cartDetails, +     incrementItem, decrementItem, removeItem, } = useShoppingCart() return ( <div> ... <section> <h1>Cart</h1> <ul> {Object.entries(cartDetails).map(([priceId, cartItem]) => ( <li key={`cart-${priceId}`}> {cartItem.name} ({cartItem.formattedPrice} * {cartItem.quantity} = {cartItem.formattedValue})<br/> + <button onClick={() => incrementItem(priceId)}>1つ増やす</button> + <button onClick={() => decrementItem(priceId)}>1つ減らす</button> + <button onClick={() => removeItem(priceId)}>削除</button> </li> ))} </ul> </section> </div> ) } これだけでカートの中身を増減や削除できるようになりました。 Stripe Checkoutにリダイレクトさせて、注文できるようにしよう 最後にカートに登録した商品を注文できるようにしましょう。 これもuseShoppingCartで取得できるメソッドを利用するだけです。 const Shop = () => { const { addItem,  cartDetails,     incrementItem, decrementItem, removeItem, + redirectToCheckout, } = useShoppingCart() return ( <div> ... + <button onClick={() => redirectToCheckout()}> + 注文する + </button> </div> ) } 追加した「注文する」ボタンをクリックすると、カートに追加した商品・料金データと個数が反映されたCheckoutページが表示されます。 Appendix: カート情報はどこに保存されている? 実装を見る限り、カートの情報はlocalStorageに保存されています。 そのため、途中でユーザーがサイトから離脱した場合でも、同じブラウザであればカートの内容を維持することができます。 終わりに use-shopping-cartの基本的な機能について紹介しました。 APIでCheckoutセッションを作成した場合など、ここでは紹介できていないユースケースもまだまだありますので、また改めて紹介したいと思います。 [PR] Stripe開発者向け情報をQiitaにて配信中! 2021年12月よりQiitaにて、Stripe開発者のためのブログ記事更新を開始しました。 [Stripe Updates]:開発者向けStripeアップデート紹介・解説 ユースケース別のStripe製品や実装サンプルの紹介 Stripeと外部サービス・OSSとの連携方法やTipsの紹介 初心者向けのチュートリアル(予定) など、Stripeを利用してオンラインビジネスを始める方法について随時更新してまいります。 -> Stripe Organizationsをフォローして最新情報をQiitaで受け取る
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vanilla JavaScriptのfetch関数にtimeoutとretryの機能を追加する方法

目的 Vanilla JavaScriptのfetch()に特定の時間が追加したら、エラーを起こして処理を終わらせる機能、そして、fetchが取得に失敗した時に特定の回数のみ再度取得を試みる機能を追加すること。 背景 fetchを使うのか、axiosを使うのか、迷うエンジニアもいると思います。基本的に、axiosを使うべきだと筆者は考えています。なぜなら、Axiosを使えば、Timeoutはすぐに設定できるのと、axios-retryというパッケージを使えばretryの機能も簡単に追加できるからです。 ただし、場合によってはaxiosを入れたくない事情もあるのかもしれません。また、筆者も経験しましたが、axios-retryが思うように動いてくれなかったりします。axiosは簡単に使えるが、何をしているかわからん、ということはよろしくない状況なので、ここでは勉強も兼ねて同じことをfetchとPromiseでどうやって再現できるかをご紹介します。 目次 環境構築 fetchを別のPromiseに包む timeoutのPromiseを返してくれる関数を作る Promise.raceを使って、fetchに時限を追加する 特定の回数、再度取得を試す機能を追加 まとめ 環境構築 実は、fetch APIはBrowserにのみ入っているパッケージなのです。node.jsの環境で実行すると、「ReferenceError: fetch is not defined」というエラーが返ってきます。(fetchをnodeで使いたい方はこちらのパッケージを使えばいいはず) なので、今回は簡単に書いたコードをBrowserで実行するために、ViteでVanilla JavaScript→TypeScriptのプロジェクトを作ります。 cd ~ && cd Documents/ mkdir vite-projects && cd vite-projects/ yarn create vite そして、project-nameを適当に書いて、Vanilla→vanilla-tsを選択。 これでVisual Studio CodeなどのEditorで作ったViteプロジェクトを開いて、そこのターミナルで yarn install && yarn dev と実行すれば、localhost:3000番でViteのストックプロジェクトが見られます。 それから、src/main.tsに入っているコードをとりあえず全て削除して保存しておきましょう。 fetchをPromiseに包む fetchってそもそもPromiseやっし..?と思っているあなた。そうです。ただ、fetchはaxiosと違って、リクエストの結果のstatusが200じゃなくてもPromiseをresolveしてしまうので、少し厄介です。200番じゃなかったら、rejectをしたいものです。 引用:MDN https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise 少し説明すると、Promiseを作ると、すぐに実行されるのはPromiseのExecutorという、Promiseを作る時に定義する関数です。このexecutorは、resolveとrejectという二つの関数をPromiseからもらいます。またexecutorを実行した後は、当Promiseは「Pending(未解決)」状態になります。 resolveは、Promiseの非同期処理が無事にできた時に使って結果を次のPromiseに渡す関数。 rejectは、何らかのエラーが発生して、エラー処理をするための情報を次のPromiseに渡す関数。 resolveもしくはrejectが実行されれば、当Promiseは「Settled(解決済み)」の状態になります。 Promiseの命の輪をまとめると 1. new PromiseでPromiseが誕生 1. 誕生後、すぐにexecutorが実行される。 1. 状態がPendingになる。 1. 条件が揃えば、resolveもしくはrejectが実行される。 1. 状態がSettledになる。 今回はresponseがokじゃなかったら、rejectでエラー処理をしたいので以下のようなPromiseを返す関数を作ります。 /src/main.ts const attemptFetch = (url: string) => new Promise((resolve, reject) => { const fetchPromise = fetch(url); fetchPromise.then((response: Response) => { if (response.ok) { resolve(response.json()); } else { reject(`Invalid Response: ${response.status}`); } }); }); 流れは以下の通り 1. fetchがPromiseを返してくれます。 1. fetchPromiseのthen()でresponseをもらい、statusが200番(response.okがtrue)だったら、attemptFetchのnew Promiseをresolveする。 1. statusが200番意外だったら、attemptFetchのnew Promiseをrejectして、エラー処理をcatchでできるようにする。 試しに、https://jsonplaceholder.typicode.com/のダミーAPIを使って実行してみましょう! attemptFetch("https://jsonplaceholder.typicode.com/todos/1") .then((data) => console.log(data)) .catch((reason) => console.error(reason)); 結果: {userId: 1, id: 1, title: 'delectus aut autem', completed: false} 次、そのAPIに存在しないアドレスでfetchしてみましょう。 attemptFetch("https://jsonplaceholder.typicode.com/not-good-url") .then((data) => console.log(data)) .catch((reason) => console.error(reason)); 結果: あれ?エラーが二つ表示されている?これは、Chromeもエラーを出しているから、二つ出ますので、上のVMのエラーを無視してもいいです。 timeoutのPromiseを返してくれる関数を作る 次、時限の機能を追加したいのですが、これを実装するために、Promise.raceをのちに使います。説明ものちに書きますが、とりあえず、特定の時間が過ぎれば、rejectでErrorを返すPromiseを先に作っておきたいです。 const makeTimeoutPromise = (timeoutLength: number) => { return new Promise((_, reject) => { setTimeout(() => { reject(Error("Fetch timeout")); }, timeoutLength); }); }; makeTimeoutPromiseは時限の設定を受けて、executorでsetTimeoutで時限が過ぎたらrejectでErrorを返してPromiseをSettledの状態にするPromiseを返してくれる関数です。 これをattemptFetch関数と併用して、Promise.raceで時限機能を実装します。 Promise.raceを使って、fetchに時限を追加する Promise.raceとは何かを説明します。 Promise.raceはPromiseのArrayをArgumentとして受け取ります。Arrayに入っているPromiseのexecutorが順番に実行され、最初にSettledの状態になるPromiseの結果を返す関数です。Settledの状態だけになればよく、resolveされたのか、rejectされたのかを見ていないのです。 今回はこの特性を活かして、timeout(時限)機能を追加するのに使います。 つまり、makeTimeoutPromiseが返してくれるPromiseが、attemptFetchが返してくれるPromiseより、早くrejectをされれば、Promise.raceもrejectされ、Settledの状態に入るという望ましい結果が得られるということです。 下(しちゃ) んかい 書(か)ちょーる コード、 見(ん)じ くぃみそーり! const fetchWithTimeout = (url: string, timeout: number) => { return Promise.race([attemptFetch(url), makeTimeoutPromise(timeout)]); }; これも実行してみましょう!まず、APIより絶対にTimeoutのPromiseが先にrejectされるように、timeoutの設定を0msにしてみましょう! fetchWithTimeout("https://jsonplaceholder.typicode.com/todos/1", 0) .then((data) => console.log(data)) .catch((reason) => console.error(reason)); 結果: 案の定失敗します。 次は、70msにしてみましょう!筆者のネット環境では、Pingと同じくらいの時間なので、半々で成功したり、timeoutしたりするべきです。 fetchWithTimeout("https://jsonplaceholder.typicode.com/todos/1", 70) .then((data) => console.log(data)) .catch((reason) => console.error(reason)); 結果: まちまちでちょうどいいです! さて、本投稿の醍醐味である、retryも追加してみましょう! 特定の回数、再度取得を試す機能を追加 これは間違いなくPromiseでやると非常にややこしいことです。 Promiseの連鎖で一回失敗したら、じゃあ、もう一度attemptFetchを実行して、そこで同じような.then, .catchを追加する、という流れも簡単にできますが、100回retryしたいというような場面になったらどうでしょう? やはり、ここはProgram的にretryができるようにしたい。 つまり、何回かfetchを試してみて、成功した時に、あるコードを実行する、特定の回数分fetchを実行してみても失敗した場合に、エラー処理をしてくれるような、何かが欲しい... 成功→resolve、失敗→reject、何だかPromiseみたいな... そうなのです!retryするロジックも含めて、全てをPromiseで包んだらできるのです! const fetchWithTimeoutAndRetries = ( url: string, timeout: number, maxRetries: number ) => { return new Promise((resolve, reject) => { let attempts = 1; const executeFetch = () => { const newFetchAttempt = fetchWithTimeout(url, timeout); newFetchAttempt .then((data) => { resolve({ data, attempts }); }) .catch((reason) => { console.error(reason, attempts); if (attempts < maxRetries) { attempts++; executeFetch(); } else { reject("All retries failed!"); } }); }; executeFetch(); }); }; fetchWithTimeoutAndRetriesはまず、Promiseを返します。返すPromiseは、executorにattemptsという変更ができる関数を定義します。そしてexecutorの中で、executeFetchという関数も定義します。executeFetchは、fetchWithTimeoutでtimeoutのPromiseとattemptFetchのPromiseをPromise.raceで実行します。そのPromise.raceがSettledの状態になれば、resolvedか、rejectedかによって違う処理をします。 resolvedの場合は、fetchWithTimeoutAndRetriesのPromiseをresolveして、APIから取得したDataを渡します。 rejectedされた場合は、親のPromiseのexecutorの関数で定義したattemptsを参照して、argumentのmaxRetriesより小さければ、もう一度executeFetchを実行します。maxRetriesよりattemptsが大きければ、fetchWithTimeoutAndRetriesのPromiseをrejectして、全てのretryが失敗したことをその後のエラー処理に知らせます。 ちなみに、Promiseのexecutorで定義しているattemptsの変数ですが、こちらはJavaScriptのスコープではexecuteFetchもアクセスできて、また、executeFetchが終わっても初期化されません。これはJavaScriptのLexical Scopeによる副作用で、実に多くのJavaScriptフレームワークがこれを利用しています。React、Vue 3のHooksなどもこのLexical ScopeでState管理をしています。 でーじ 面白(うむ)さ くとぅ、くりん 勉強(びんちょう) さびら! 上記のコードは筆者が週末、随分思い悩んで思いついた結果です。簡単そうに見えても、随分苦労しました... まとめ これで、Promiseを使ってfetchに時限機能、並びに再取得の機能を追加する方法を解説してきました。 Promiseは、奥が深いけれど、基礎をしっかり勉強すればわかるものです。 恐れずに、Promiseを勉強していきましょう!びんちょう、ちばりよー!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのFetch APIにtimeoutとretryの機能を追加する方法

目的 Vanilla JavaScriptのfetch()に特定の時間が追加したら、エラーを起こして処理を終わらせる時限機能、そして、fetchが取得に失敗した時に特定の回数のみ再度取得を試みる機能を追加すること。 背景 fetchを使うのか、axiosを使うのか、迷うエンジニアもいると思います。基本的に、axiosを使うべきだと筆者は考えています。なぜなら、Axiosを使えば、Timeoutはすぐに設定できるのと、axios-retryというパッケージを使えばretryの機能も簡単に追加できるからです。 ただし、場合によってはaxiosを入れたくない事情もあるのかもしれません。また、筆者も経験しましたが、axios-retryが思うように動いてくれなかったりします。axiosは簡単に使えるが、何をしているかわからん、ということはよろしくない状況なので、ここでは勉強も兼ねて同じことをfetchとPromiseでどうやって再現できるかをご紹介します。 目次 環境構築 fetchを別のPromiseに包む timeoutのPromiseを返してくれる関数を作る Promise.raceを使って、fetchに時限を追加する 特定の回数、再度取得を試す機能を追加 まとめ 環境構築 実は、fetch APIはBrowserにのみ入っているパッケージなのです。node.jsの環境で実行すると、「ReferenceError: fetch is not defined」というエラーが返ってきます。(fetchをnodeで使いたい方はこちらのパッケージを使えばいいはず) なので、今回は簡単に書いたコードをBrowserで実行するために、ViteでVanilla JavaScript→TypeScriptのプロジェクトを作ります。 cd ~ && cd Documents/ mkdir vite-projects && cd vite-projects/ yarn create vite そして、project-nameを適当に書いて、Vanilla→vanilla-tsを選択。 これでVisual Studio CodeなどのEditorで作ったViteプロジェクトを開いて、そこのターミナルで yarn install && yarn dev と実行すれば、localhost:3000番でViteのストックプロジェクトが見られます。 それから、src/main.tsに入っているコードをとりあえず全て削除して保存しておきましょう。 fetchをPromiseに包む fetchってそもそもPromiseやっし..?と思っているあなた。そうです。ただ、fetchはaxiosと違って、リクエストの結果のstatusが200じゃなくてもPromiseをresolveしてしまうので、少し厄介です。200番じゃなかったら、rejectをしたいものです。 引用:MDN https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise 少し説明すると、Promiseを作ると、すぐに実行されるのはPromiseのExecutorという、Promiseを作る時に定義する関数です。このexecutorは、resolveとrejectという二つの関数をPromiseからもらいます。またexecutorを実行した後は、当Promiseは「Pending(未解決)」状態になります。 resolveは、Promiseの非同期処理が無事にできた時に使って結果を次のPromiseに渡す関数。 rejectは、何らかのエラーが発生して、エラー処理をするための情報を次のPromiseに渡す関数。 resolveもしくはrejectが実行されれば、当Promiseは「Settled(解決済み)」の状態になります。 Promiseの命の輪をまとめると 1. new PromiseでPromiseが誕生 1. 誕生後、すぐにexecutorが実行される。 1. 状態がPendingになる。 1. 条件が揃えば、resolveもしくはrejectが実行される。 1. 状態がSettledになる。 今回はresponseがokじゃなかったら、rejectでエラー処理をしたいので以下のようなPromiseを返す関数を作ります。 /src/main.ts const attemptFetch = (url: string) => new Promise((resolve, reject) => { const fetchPromise = fetch(url); fetchPromise.then((response: Response) => { if (response.ok) { resolve(response.json()); } else { reject(`Invalid Response: ${response.status}`); } }); }); 流れは以下の通り 1. fetchがPromiseを返してくれます。 1. fetchPromiseのthen()でresponseをもらい、statusが200番(response.okがtrue)だったら、attemptFetchのnew Promiseをresolveする。 1. statusが200番意外だったら、attemptFetchのnew Promiseをrejectして、エラー処理をcatchでできるようにする。 試しに、https://jsonplaceholder.typicode.com/のダミーAPIを使って実行してみましょう! attemptFetch("https://jsonplaceholder.typicode.com/todos/1") .then((data) => console.log(data)) .catch((reason) => console.error(reason)); 結果: {userId: 1, id: 1, title: 'delectus aut autem', completed: false} 次、そのAPIに存在しないアドレスでfetchしてみましょう。 attemptFetch("https://jsonplaceholder.typicode.com/not-good-url") .then((data) => console.log(data)) .catch((reason) => console.error(reason)); 結果: あれ?エラーが二つ表示されている?これは、Chromeもエラーを出しているから、二つ出ますので、上のVMのエラーを無視してもいいです。 timeoutのPromiseを返してくれる関数を作る 次、時限の機能を追加したいのですが、これを実装するために、Promise.raceをのちに使います。説明ものちに書きますが、とりあえず、特定の時間が過ぎれば、rejectでErrorを返すPromiseを先に作っておきたいです。 const makeTimeoutPromise = (timeoutLength: number) => { return new Promise((_, reject) => { setTimeout(() => { reject(Error("Fetch timeout")); }, timeoutLength); }); }; makeTimeoutPromiseは時限の設定を受けて、executorでsetTimeoutで時限が過ぎたらrejectでErrorを返してPromiseをSettledの状態にするPromiseを返してくれる関数です。 これをattemptFetch関数と併用して、Promise.raceで時限機能を実装します。 Promise.raceを使って、fetchに時限を追加する Promise.raceとは何かを説明します。 Promise.raceはPromiseのArrayをArgumentとして受け取ります。Arrayに入っているPromiseのexecutorが順番に実行され、最初にSettledの状態になるPromiseの結果を返す関数です。Settledの状態だけになればよく、resolveされたのか、rejectされたのかを見ていないのです。 今回はこの特性を活かして、timeout(時限)機能を追加するのに使います。 つまり、makeTimeoutPromiseが返してくれるPromiseが、attemptFetchが返してくれるPromiseより、早くrejectをされれば、Promise.raceもrejectされ、Settledの状態に入るという望ましい結果が得られるということです。 下(しちゃ) んかい 書(か)ちょーる コード、 見(ん)じ くぃみそーり! const fetchWithTimeout = (url: string, timeout: number) => { return Promise.race([attemptFetch(url), makeTimeoutPromise(timeout)]); }; これも実行してみましょう!まず、APIより絶対にTimeoutのPromiseが先にrejectされるように、timeoutの設定を0msにしてみましょう! fetchWithTimeout("https://jsonplaceholder.typicode.com/todos/1", 0) .then((data) => console.log(data)) .catch((reason) => console.error(reason)); 結果: 案の定失敗します。 次は、70msにしてみましょう!筆者のネット環境では、Pingと同じくらいの時間なので、半々で成功したり、timeoutしたりするべきです。 fetchWithTimeout("https://jsonplaceholder.typicode.com/todos/1", 70) .then((data) => console.log(data)) .catch((reason) => console.error(reason)); 結果: まちまちでちょうどいいです! さて、本投稿の醍醐味である、retryも追加してみましょう! 特定の回数、再度取得を試す機能を追加 これは間違いなくPromiseでやると非常にややこしいことです。 Promiseの連鎖で一回失敗したら、じゃあ、もう一度attemptFetchを実行して、そこで同じような.then, .catchを追加する、という流れも簡単にできますが、100回retryしたいというような場面になったらどうでしょう? やはり、ここはProgram的にretryができるようにしたい。 つまり、何回かfetchを試してみて、成功した時に、あるコードを実行する、特定の回数分fetchを実行してみても失敗した場合に、エラー処理をしてくれるような、何かが欲しい... 成功→resolve、失敗→reject、何だかPromiseみたいな... そうなのです!retryするロジックも含めて、全てをPromiseで包んだらできるのです! const fetchWithTimeoutAndRetries = ( url: string, timeout: number, maxRetries: number ) => { return new Promise((resolve, reject) => { let attempts = 1; const executeFetch = () => { const newFetchAttempt = fetchWithTimeout(url, timeout); newFetchAttempt .then((data) => { resolve({ data, attempts }); }) .catch((reason) => { console.error(reason, attempts); if (attempts < maxRetries) { attempts++; executeFetch(); } else { reject("All retries failed!"); } }); }; executeFetch(); }); }; fetchWithTimeoutAndRetriesはまず、Promiseを返します。返すPromiseは、executorにattemptsという変更ができる関数を定義します。そしてexecutorの中で、executeFetchという関数も定義します。executeFetchは、fetchWithTimeoutでtimeoutのPromiseとattemptFetchのPromiseをPromise.raceで実行します。そのPromise.raceがSettledの状態になれば、resolvedか、rejectedかによって違う処理をします。 resolvedの場合は、fetchWithTimeoutAndRetriesのPromiseをresolveして、APIから取得したDataを渡します。 rejectedされた場合は、親のPromiseのexecutorの関数で定義したattemptsを参照して、argumentのmaxRetriesより小さければ、もう一度executeFetchを実行します。maxRetriesよりattemptsが大きければ、fetchWithTimeoutAndRetriesのPromiseをrejectして、全てのretryが失敗したことをその後のエラー処理に知らせます。 ちなみに、Promiseのexecutorで定義しているattemptsの変数ですが、こちらはJavaScriptのスコープではexecuteFetchもアクセスできて、また、executeFetchが終わっても初期化されません。これはJavaScriptのLexical Scopeによる副作用で、実に多くのJavaScriptフレームワークがこれを利用しています。React、Vue 3のHooksなどもこのLexical ScopeでState管理をしています。 でーじ 面白(うむ)さ くとぅ、くりん 勉強(びんちょう) さびら! 上記のコードは筆者が週末、随分思い悩んで思いついた結果です。簡単そうに見えても、随分苦労しました... まとめ これで、Promiseを使ってfetchに時限機能、並びに再取得の機能を追加する方法を解説してきました。 Promiseは、奥が深いけれど、基礎をしっかり勉強すればわかるものです。 恐れずに、Promiseを勉強していきましょう!びんちょう、ちばりよー!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

僕らはファイナライズを待たなければいけないのか~Symbolブロックチェーンの場合~

即時ファイナリティ。 分散化を徹底したブロックチェーンほど、縁の遠い存在と考えがちな言葉です。 例えば取引所の多くは、ブロックが覆らないと思われる時間を待って入金を確定させます。 ブロックチェーンを従来システムに組み込むためには、待たなければいけない時間がある。 と思っているなら、あなたの頭はまだWeb2.0を彷徨っています。 少し立ち止まって考えてみましょう。ブロックチェーンは第三者の仲介無しに送金を実現します。送金指示はあなたに直接投げられたメッセージです。たとえそれが未承認状態だったとしても、情報送信者の意思表示を証明するために必要な情報はすべて世界中に公開されているのです。 他チェーン同士が自律分散的にトークン交換するならまだしも、特定の組織によって管理されたシステム系への送信であれば、利益を享受する情報の受け手がトランザクションの信頼性向上に歩み寄ることができます。 何が言いたいかというと、ブロックチェーンがロールバックしようが、送金メッセージを受信して利益を得る権利を得た人が、そのチャンスが確信に変わるまで何度もアナウンスすればいいのです。 というわけで、今回はSymbolブロックチェーンのロールバックとファイナライズを検知し再アナウンスするための方法を紹介します。 latestBlock = 0; lastFinalizedBlock = 0; unfinalizedTxes = []; listener.open().then(() => { listener.newBlock() .pipe( op.mergeMap(x=>blockRepo.getBlockByHeight(x.height)), ) .subscribe(b=>{ console.log("------------------"); if(latestBlock >= b.height.compact()){ console.log("■■■ roll back!! ■■■" + latestBlock + "->" + b.height.compact()); rollbackBlocks = Array.from(Array(latestBlock - b.height.compact() + 1), (v, k) => k + b.height.compact()); console.log(rollbackBlocks); checkTxes = unfinalizedTxes.filter(tx=>rollbackBlocks.indexOf(tx.transactionInfo.height.compact()) >= 0); console.log(checkTxes); for(checkTx of checkTxes){ rxjs.of({ existTxes:txRepo.getTransactionsById([checkTx.transactionInfo.hash],sym.TransactionGroup.Confirmed), checkTx:checkTx }) .subscribe(async obs=>{ if(obs.existTxes.length > 0){ console.log("on chain"); }else{ console.log("==>■DROP TRANSACTION!!!!■"); signedTx = new sym.SignedTransaction( obs.checkTx.serialize(), obs.checkTx.transactionInfo.hash, obs.checkTx.signer.publicKey, obs.checkTx.type, obs.checkTx.networkType ) clog(signedTx); //ネットワークへアナウンス try { res = await transactionService.announce(signedTx, listener).toPromise(); console.log(res); } catch(err) { console.log(err); } finally { } } }); } } console.log("prevHash:" + b.previousBlockHash); console.log("timestamp:" + new Date(Number(b.timestamp.toString()) + epochAdjustment * 1000)); console.log(b.height.compact() + ":("+ b.transactionsCount +"txs):"+ b.hash); latestBlock = b.height.compact(); txRepo.search({group:sym.TransactionGroup.Confirmed,height:b.height.compact(),pageSize:100 }) .subscribe(tx=>{ unfinalizedTxes = unfinalizedTxes.concat(tx.data); console.log(unfinalizedTxes) }) }); listener.finalizedBlock() .subscribe(fb=>{ diff_max = latestBlock - lastFinalizedBlock; diff_min = latestBlock - fb.height.compact(); if(lastFinalizedBlock != 0){ console.log("================"); console.log("FinalizedBlock:" + fb.height.compact() + ":" + diff_min.toString() +"<" + diff_max.toString() + ":" + fb.hash); finalizedBlocks = Array.from(Array(fb.height.compact() - lastFinalizedBlock), (v, k) => k + lastFinalizedBlock + 1); console.log(finalizedBlocks); unfinalizedTxes = unfinalizedTxes.filter(tx=>finalizedBlocks.indexOf(tx.transactionInfo.height.compact()) ==-1); console.log(unfinalizedTxes); } lastFinalizedBlock = fb.height.compact(); }) }); 解説 基本構成はこちらです。 listener.newBlock() .pipe( op.mergeMap(x=>blockRepo.getBlockByHeight(x.height)), ) .subscribe(b=>{ console.log("------------------"); if(latestBlock >= b.height.compact()){ console.log("■■■ roll back!! ■■■" + latestBlock + "->" + b.height.compact()); } latestBlock = b.height.compact(); }); listener.finalizedBlock() .subscribe(fb=>{ diff_max = latestBlock - lastFinalizedBlock; diff_min = latestBlock - fb.height.compact(); if(lastFinalizedBlock != 0){ console.log("================"); console.log("FinalizedBlock:" + fb.height.compact() + ":" + diff_min.toString() +"<" + diff_max.toString() + ":" + fb.hash); } lastFinalizedBlock = fb.height.compact(); }) listener.newBlock()で新規に生成されたblockを検知します。 listener.finalizedBlock()でファイナライズブロックを検知します。 今回は新規ブロックで承認された全トランザクションをキャッシュし、ブロックがファイナライズされたらキャッシュから解放します。途中でロールバックが発生した場合、キャッシュしていたトランザクション情報を用いてノードに再アナウンスを行います。 キャッシュへのトランザクション追加 txRepo.search({group:sym.TransactionGroup.Confirmed,height:b.height.compact(),pageSize:100 }) .subscribe(tx=>{ unfinalizedTxes = unfinalizedTxes.concat(tx.data); console.log(unfinalizedTxes) }) 新規に生成されたブロックに含まれるトランザクションをunfinalizedTxesに格納します。 ファイナライズされたトランザクションの解放 finalizedBlocks = Array.from(Array(fb.height.compact() - lastFinalizedBlock), (v, k) => k + lastFinalizedBlock + 1); unfinalizedTxes = unfinalizedTxes.filter(tx=>finalizedBlocks.indexOf(tx.transactionInfo.height.compact()) ==-1); キャッシュしたトランザクション情報のブロックがファイナライズブロックだった場合にキャッシュから解放します。 ロールバック内に含まれるトランザクションを抽出 rollbackBlocks = Array.from(Array(latestBlock - b.height.compact() + 1), (v, k) => k + b.height.compact()); checkTxes = unfinalizedTxes.filter(tx=>rollbackBlocks.indexOf(tx.transactionInfo.height.compact()) >= 0); ロールバックしたブロックの配列を生成します。 キャッシュからそのブロック高を含むトランザクションをcheckTxesに代入します。  キャッシュからロールバックしたチェーンに含まれないトランザクションを抽出 for(checkTx of checkTxes){ rxjs.of({ existTxes:txRepo.getTransactionsById([checkTx.transactionInfo.hash],sym.TransactionGroup.Confirmed), checkTx:checkTx }) .subscribe(async obs=>{ if(obs.existTxes.length > 0){ console.log("on chain"); }else{ console.log("==>■DROP TRANSACTION!!!!■"); } }); } checkTxes内のトランザクションの存在をノードに問い合わせ存在しない場合は再アナウンスの必要なトランザクションとして把握します。 キャッシュしたトランザクションから署名済みトランザクションを構築して再アナウンス signedTx = new sym.SignedTransaction( obs.checkTx.serialize(), obs.checkTx.transactionInfo.hash, obs.checkTx.signer.publicKey, obs.checkTx.type, obs.checkTx.networkType ) clog(signedTx); //ネットワークへアナウンス try { res = await transactionService.announce(signedTx, listener).toPromise(); console.log(res); } catch(err) { console.log(err); } finally { } これで、ロールバック発生時に消失の可能性があるトランザクションを、再アナウンスすることができました。 留意点 上記サンプルプログラムはアグリゲートトランザクションには対応しておりません。内部トランザクションまでキャッシュするにはノードに対して別途抽出作業が必要です。 また、Symbolの場合、ロールバックで覆されたブロックにしか含まれなかったトランザクションは未承認トランザクションに差し戻されます。 したがって、上記のような手続きを踏む必要もなく次のブロックで承認される場合がほとんどで、再アナウンスしたトランザクションは二重支払いのチェックにて無効になります。 ただ、ノードが保持するキャッシュサイズを超えるような大幅なロールバックが発生した場合は未承認トランザクションに戻らないトランザクションが含まれるため、万全を期したい場合は有効な手段となりうるでしょう。また、期限切れを迎えてしまったトランザクションについても再アナウンスができないことにもご注意ください(その前に確定的ファイナライズを迎えるとは思いますが)。 さいごに 今後、様々なコンソーシアムがパブリックチェーンの採用を検討するでしょう。ブロックチェーンはファイナライズ機構が弱いから使えない、というのは非常に受け身な考え方です。コンソーシアム間で流通するトークンに関するトランザクションのみを監視して万が一のTXドロップを拾い上げればほとんどの懸念は解消されます。万が一の危険にたじろいで多大なビジネス変革のチャンスを逃してはいけません。 ファイナライズとは、 「つかみかけたチャンスを逃がさないために努力することを止めてもいい時間」 という意味に読み替えましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む