- 投稿日:2022-02-24T23:36:40+09:00
Deno Deep Dive - Denoでのアプリケーション実装Tips / Node.jsとの比較
概要 本記事ではNode.jsに代わるサーバサイドJavaScriptランタイムであるDenoについて、実際のアプリケーション実装で得られた知見をご紹介します。 まずはDenoの概要について説明し、アプリケーションを実装する上でのDenoの便利な使い方やAPI、各種ツールについて解説していきます。最後に筆者がDenoによる開発を経てNode.jsによる開発と比較した際のDenoの優れている点/不足している点を挙げていきます。 Denoとは DenoはNodejsの開発者であるRyan DahlがNode.jsの反省を活かして作り出したJavaScriptランタイムです。 特徴としては以下のものがあります。 TypeScriptがout of box、つまり設定なしで実行できる ES Modules対応で依存モジュールはURLによってインポートする 権限管理が厳密 フォーマッタやテストランナなどの便利機能がランタイムに付属 まずTypeScriptについて、DenoはTypeScriptで書いたスクリプトを直接実行することができます。例えばちょっとした作業スクリプトを書く場合にも、または本格的なアプリケーションを書く場合にも設定なしですぐにTypeScriptが利用できます。スクリプトを実行する際に事前にトランスパイルやビルドを実行する必要はありません。内部で実行されるDenoのコンパイラはRustで実装されており、高速でトランスパイル/ビルドされます。 DenoはES Modulesを活用したモジュールシステムを採用しており、外部モジュールはURLを使ってインポートしていきます。Node.jsでは外部モジュールはプロジェクトフォルダ内のnode_modulesにダウンロードしていました。Denoは実行時にURLから読み込めばいいのでプロジェクト内に巨大なnode_modulesフォルダを作る必要がありません。もちろん実行のたびにダウンロードすると時間がかかってしまうのでしまうので、ランタイムがキャッシュしてくれます。 Denoではネットワークやファイルシステムに対するアクセス制御を行うことができます。明示的に指定しない限りそれらへのアクセスはできない仕様です。Node.jsと比べてセキュアなランタイムと言えます。 またDenoのランタイムにフォーマッタやテストランナが付属しており、外部ライブラリをインストールする必要なくdenoコマンドによって実行可能です。この辺りの具体的な使い方についても後述します。 Denoでのアプリケーション実装Tips 実装事例 弊社がリリースしたDockpit liteというツールのバックエンドAPIをDenoで実装しました。 DenoのHTTP APIミドルウェアであるoakを活用し、認証や別APIからのデータの取得/加工などの機能を実装しています。 以下ではこのAPIサーバの実装で利用したDenoの機能やユーティリティ、外部ライブラリの使い方を説明していきます。 付属機能/API アプリケーションの実行 DenoではJavaScript/TypeScriptをdeno runコマンドで実行します。TypeScriptで書いたスクリプトを直接実行できます。Node.jsではtsconfig.jsonを書いたり、webpackなどモジュールバンドラの設定をしたりしなければTypeScriptを実行することはできませんでした。 deno run index.ts deno runコマンドにオプションを指定しない限りネットワーク/ファイルシステム/環境変数などにアクセスすることはできません。以下のようにフラグを付与して実行することでアクセス許可を与えることができます。またファイルシステムの読み書きなどについてはアクセスを許可する範囲を限定することが可能です。 deno run --allow-net --allow-read=. --allow-write=./log index.ts 依存パッケージのインポート Denoでは依存パッケージはURLからインポートします。 import { decode, encode } from 'https://deno.land/std@0.120.0/encoding/base64url.ts'; import { Application } from 'https://deno.land/x/oak@v9.0.1/mod.ts'; Deno標準のライブラリとよく利用されるサードパーティのライブラリはdeno.landにアップロードされています。 std@0.126.0 | Deno Third Party Modules | Deno 例えばAPIミドルウェアのoakライブラリをインポートする場合はhttps://deno.land/x/oak@v[version]/mod.tsを指定します。ここに置かれているソースコードはブラウザ上でも閲覧できるようになっています。またES Modulesなのでソースコードのモジュールファイル(mod.ts)から直接必要なものをインポートできます。つまり、ブラウザ上でライブラリのソースコードを閲覧して必要なものを直接URLによってインポートできるということです。このURLインポートとES Modulesの組み合わせによってライブラリを活用した開発体験が良いのもDenoの特徴です。 内部モジュールのインポート 内部モジュールはNode.jsと同様にプロジェクトフォルダ内の相対パスでインポートできます。ただしDenoではインポートするモジュールの拡張子が必要な点に注意が必要です。 import { ApiError } from './utils/Error.ts'; またRyan DahlがNode.jsの反省点に挙げていたように、Denoにはフォルダ内のindex.jsをそのフォルダ名でインポートする機能はありません。ファイルシステムの構造通りにインポートします。 インポートマップ さて、以上で述べたようなURLでのインポートや相対パスでのインポートを毎回書くのは面倒です。そこでDenoにはインポートマップという機能があります。 Import maps | Manual | Deno この機能によって、絶対パスでのモジュールのインポートや外部ライブラリインポートURLの省略(エイリアスの作成)ができます。以下のようなJSONにエイリアスとその内容を記述していきます。 importMap.json { "imports": { "@/": "./", "oak": "https://deno.land/x/oak@v9.0.1/mod.ts" } } このインポートマップのパスを実行時にオプションで指定します。 deno run --importmap=importMap.json index.ts このように設定することによって、以下のようにインポートできるようになります。 import { Application } from 'oak'; import { ApiError } from '@/utils/Error.ts'; インポートマップの機能は記述の省略以外にも依存ライブラリのバージョンの管理に役立ちます。アプリケーション全体でライブラリのバージョンを変更したい場合はインポートマップの記述のみを修正すれば良いからです。インポートマップを利用せずにそれぞれのモジュールでバージョンを記述しているとその全てを修正する必要があります。 watchモード こうして様々なライブラリやモジュールを利用しながら開発を進めていきます。開発中のアプリケーションを更新するためにはdeno runを再実行する必要がありますが、これは非常に面倒です。Denoにはwatchモードがあり、ソースの変更を検知して自動でアプリケーションを再起動してくれます。 Command line interface | Manual | Deno deno run --watch index.ts 環境変数 完成したアプリケーションをデプロイするに際して、対象の環境に応じて環境変数を変更して動作を変えることがよくあります。Denoでは標準APIのDeno.envによって環境変数を利用することができます。 > Deno.env.set('ENVIRONMENT', 'development'); > Deno.env.get('ENVIRONMENT'); "development" また.envファイルに記述した環境変数を読み込むならdotenvライブラリが利用できます。 .env.development ENVIRONMENT="development" import { config } from "https://deno.land/x/dotenv@v3.2.0/mod.ts"; config({path: './.env.development', export: true}); Deno.env.get('ENVIRONMENT'); Denoで環境変数利用する場合、実行時に--allow-envフラグを付ける必要があります。逆に言えばこのフラグを付与しなければ意図しない環境変数の読み取り/変更を防ぐことができます。 deno run --allow-env index.ts トップレベルawait Node.js 14.8以降で使えるトップレベルawaitがDenoでもデフォルトで利用できます。トップレベルawaitとは、スクリプト実行時に非同期関数外でawaitできる機能です。例えば以下のようなコードを実行することができます。 const data = await fetch('https://localhost:8000/user/get', { method: 'POST', body: JSON.stringify({ userid: 1 }), }); console.log(data); console.log(await data.json()); この機能により、非同期処理やPromiseオブジェクトの扱いが簡単になります。 フォーマッタ DenoにはJavaScriptでいうprettierにあたるフォーマッタが標準搭載されています。 Formatter | Manual | Deno deno fmtコマンドで実行してソースコードの整形が行えます。細かいフォーマッタの設定は以下のようにdeno.jsonに記述します。 deno.json { "fmt": { "options": { "useTabs": false, "singleQuote": true, "indentWidth": 4, "lineWidth":120 } } } この設定ファイルを利用してフォーマットする際は実行時のオプションで設定ファイルへのパスを指定します。 deno fmt --config ./deno.json テストランナ Denoにはテストランナが標準搭載されています。Node.jsでいうJESTといった外部ライブラリのインストールは不要です。 Testing | Manual | Deno deno testコマンドでテストコードを実行できます。自動でプロジェクトフォルダ内のjs/tsファイルがチェックされ、テストコードが見つかると実行されます。テストコードの記述にはDeno.testAPIを利用します。 test.ts import { assertEquals } from "https://deno.land/std@0.126.0/testing/asserts.ts"; Deno.test("test 1", () => { const x = 1 + 2; assertEquals(x, 3); }); Deno.test("test 2", () => { const x = 1 + 2; assertEquals(x, 5); }); $ deno test running 2 tests from file:///.../test.ts test test 1 ... ok (10ms) test test 2 ... FAILED (13ms) failures: test 2 AssertionError: Values are not equal: [Diff] Actual / Expected - 3 + 5 ... またネットワークやファイルにアクセスするテストを実行する場合にはdeno runと同様に各種オプションをつけてdeno testを実行する必要があります。 外部ツール スクリプトランナ 以上で見たように、細かい設定を行なった上でDenoを実行するためにはdeno runやdeno testに様々なオプションを付ける必要があります。そうした場合に以下のようにdeno runコマンドが長くなってしまいます。 deno run --watch --importmap=importMap.json --allow-net --allow-env --allow-read=. --allow-write=./log index.ts これを解決してくれるのがスクリプトランナーのvelociraptorです。 次のようなyamlを定義しておくとコマンド実行時のフラグ、インポートマップ、環境変数ファイルを指定することができます。複数のコマンドを使い分ける場合にもそれぞれにオプションを記述できて便利です。 velociraptor.yaml allow: - net - env scripts: start-dev: cmd: deno run index.ts envFile: .env.development watch: true imap: importMap.json allow: - read=. - write=./log start: cmd: deno run index.ts envFile: .env.production watch: true imap: importMap.json allow: - read=. - write=/var/log velociraptorをインストールした上で、以下のコマンドで設定ファイルに指定した各コマンドを実行できます。Node.jsでいうpackage.jsonへのコマンドの記述のイメージです。 vr start-dev vr start velociraptorは実際には設定ファイルと.envファイルを読み取って以下のようなコマンドを生成しています。こちらを利用する場合は環境変数はファイルから読み取ってあらかじめシェルの環境変数にセットされているので、dotenvライブラリは不要になります。Deno上で.evnファイルの環境変数を利用する場合は直接Deno.env.get()するだけです。 ENVIRONMENT="development" \ deno run \ --watch --importmap=importMap.json --allow-net --allow-env --allow-read=. --allow-write=./log \ index.ts "$@" サーバ環境でvelociraptorをインストールしたくない場合はvr exportで設定した各コマンドをシェルスクリプトに展開しておくことができます。そのシェルスクリプトをサーバにデプロイして実行するだけでローカル環境と同じ動作になります。 vr export start-dev vr export start エディタ 開発するエディタとしてVSCodeを想定します。Denoで実行するTypeScriptを普通に開いても、VSCodeはNode.js用の構文解析をしてしまいます。Deno用の構文解析をさせるにはVSCodeにvscode-deno拡張機能をインストールする必要があります。 ワークスペースでDenoの拡張機能を有効にするためには.vscode/settings.jsonに以下のように記述します。 .vscode/settings.json { "deno.enable": true, "deno.lint": true, "deno.unstable": true, "deno.config": "deno.json", "deno.suggest.imports.hosts": { "https://deno.land": true }, "deno.importMap": "./importMap.json", "editor.formatOnSave": true, "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno" } } ここにインポートマップの指定もしておくと、インポートパスをエイリアスで指定していてもエディタ上でエラー表示になりません。 またeditor.formatOnSaveを有効にしてファイルを自動フォーマットさせる際に先述のDeno付属用のフォーマッタの設定を利用するにはdeno.configの項目で設定ファイル(deno.json)を指定しておきます。こうすると毎回deno fmtを実行しなくてもファイルの保存と同時にフォーマットすることができます。 Node.jsとの比較 Denoは以下の点でNode.jsより優れていると考えます。 TypeScriptを利用した際の開発体験の向上 デプロイの簡略化 権限管理の厳密化 Node.jsと比較した際に、やはりTypeScriptで書いたスクリプトを直接実行できる点によって開発体験が向上しているのを感じます。トランスパイラやモジュールバンドラといった依存要素が大幅に減っていることで、アプリケーションの実装を始めるまでの時間的/心理的ハードルが低くなっています。 またTypeScriptを直接実行可能なことでDenoは「スクリプト言語としてのTypeScript」を発明したといってもいいかもしれません。業務上簡単なスクリプトを書いて仕事を効率化する際にもTypeScriptを利用することができます。ざっくり言うとDeno+TypeScriptはPythonに近い使用感があります。この点においてもDenoは非常に便利だと感じています。 また実装した成果物をサーバにデプロイする際に、Node.jsではソースコードを配置した上で依存ライブラリのインストール、ビルド、アプリケーションの起動と3プロセスが必要です。一方でDenoではソースコードを配置してそれを直接実行できます。ライブラリのインストールとビルドの2プロセスが省略されます。 もちろん上記の二つのポイントについてはTypeScriptやECMAScriptを利用しない際はNode.jsとDenoでそれほど差異はありません。しかしながら現代において複雑なアプリケーションをJavaScriptを実装する際にそれらを利用しないことはほとんど考えられないでしょう。 最後に権限管理について、Denoではネットワークやファイル、環境変数へのアクセスに明示的な許可が必要です。不必要な権限を与えることがないのでNode.jsで実装した場合よりセキュアになります。筆者は幸いにもNode.jsアプリケーションに対するハッキング被害にあったことはありませんが、やはり権限の管理は厳しいに越したことはないでしょう。 一方で以下の点でNode.jsに及んでいないと考えます。 サードパーティライブラリのエコシステム フロントエンド開発での利用 Node.jsの歴史は長く、膨大な数のサードパーティライブラリが存在します。一方のDenoにもある程度サードパーティライブラリは揃っているものの、総数では及ぶべくもありません。ただしサードパーティライブラリがないなら自分で実装すればいい、という発想もあり得ます。筆者もCookieの署名やJWTの検証をDeno標準のエンコードライブラリを用いて実装したりしました。 またReactなどフロントエンド開発フレームワークを用いる場合は依然としてNode.jsを利用する必要があります。DenoでもAlephといったフロントエンド開発のフレームワークが開発されていますが、未だベータ版です。 これらの点については今後のエコシステムの発達によってNode.jsに追いつくこともできるかもしれません。 おわりに 本記事ではアプリケーションを実装する上でのDenoの便利な機能や外部ツールを解説しました。また現時点でのDenoとNode.jsの優劣についても述べていきました。 個人的にはJS/TSでのサーバサイドのアプリケーションの実装や日々の業務効率化スクリプト実装においてDenoを積極的に利用していきたいと考えています。本記事で関心を持たれた方は是非利用してみてください。
- 投稿日:2022-02-24T22:01:43+09:00
javascript_const、letの変数宣言とテンプレート文字列、アロー関数
React講座を受講しているため、自分のメモとしてまとめます。 var変数は簡単に上書きされることと再宣言されることで、大規模な開発ではミスの原因となります。 let変数: 上書き可能、再宣言は不可能 const変数: 上書きも再宣言も不可能 JavaScriptのオブジェクトや配列については値を変えることができます。 constで定義したオブジェクトはプロパティの変更が可能 constで定義した配列は中身の変更が可能 JSの中に基本constを使います。上書きが必要な変数についてはletを使います。 テンプレート文字列:JSの文字列にコードを埋め込んだ感じです。 構文は${}になります。 アロー関数 ・引数は()を省略することができる ・returnを省略することができる {}は書かない 分割代入
- 投稿日:2022-02-24T21:01:03+09:00
備忘録
新規コンテンツを「ふわっ」と挿入する。 $content.appendTo('#infscroll').hide().fadeIn(2000);
- 投稿日:2022-02-24T20:41:52+09:00
reduce()について
reduce()とは typescriptでreduceというものに初めて出会った。 そもそも純粋なjavascriptですら齧った程度だったのでtypescriptには大分へし折られた。 map()やfilter()などであれば何となく知ってはいたが、reduceは知らなかったので備忘録として残す。 聞くところによると初見殺しらしい。 interface Example { id: number; name: string; beer: string; } const data: Example[] = [ { id: 1, name: 'aaa', beer: 'heineken' }, { id: 2, name: 'bbb', beer: 'badwiser' }, { id: 3, name: 'ccc', beer: 'kirin' }, { id: 4, name: 'ddd', beer: 'sapporo' }, { id: 5, name: 'eee', beer: 'corona' }, ]; 例えばこういったinterfaceと配列が定義されているとする。 const total = data.reduce((acc: number, val) => { return acc + val.id; }, 0); reduceでこんな感じにできる。 accはアキュムレーターと呼ぶらしい。返却する型を設定する。 最後の引数は初期値。 idを全員分足して返却した。 totalは15となる。 const beers = ['heineken', 'sapporo', 'kirin', 'asahi', 'corona', 'ebisu']; またこういった配列がやってきたとして、 interface Beer { name: string; beer: string; } こういったinterfaceを作成し、 const beer = beers.reduce((acc: Beer[], val) => { const result = data.find((item) => item.beer === val); if (result) { acc.push({ name: result.name, beer: result.beer }); } return acc; }, []); アキュムレーターことaccをBeer[]に設定し、reduceの中でfindを使い、一致するものをBeer[]の方にして返却することができる。 beerの中は下記のようになる。 [ { "name": "aaa", "beer": "heineken" }, { "name": "ddd", "beer": "sapporo" }, { "name": "ccc", "beer": "kirin" }, { "name": "eee", "beer": "corona" } ] ちなみに、 interface MadeIn { madein: { country: string; taste: string; }; } こういった型で、 const data_2: MadeIn[] = [ { madein: { country: 'germany', taste: 'good', }, }, { madein: { country: 'usa', taste: 'good', }, }, { madein: { country: 'japan', taste: 'good', }, }, { madein: { country: 'japan', taste: 'good', }, }, { madein: { country: 'mexico', taste: 'good', }, }, ]; 配列でなくオブジェクトだった場合、 interface Country { country: string; } const country = Object.values(data_2).reduce((acc: Country[], m) => { if (m.madein.country === 'japan') { acc.push({ country: m.madein.country }); } return acc; }, []); [ { "country": "japan" }, { "country": "japan" } ] Object.values またはObject.keysなどで繋げてやれば使用することができる。 超初歩的かもしれないが初めて知った時驚愕した。 確かに初見殺しであり半殺しくらいにはされたが仕組みが理解できればとても便利だ。 ビールはサッポロ派だ。
- 投稿日:2022-02-24T19:27:04+09:00
JavaScriptの小技集
テンプレート文字列 const name = "hoge"; const age = "30"; const message = "私は" + name + "です。" + age + "歳です。" const message2 = `私は${hoge}です。${age}歳です。` message2がテンプレート文字列を使ったもの。 見やすく、コーディングしやすくなっている。 アロー関数 従来 const func1 = function(str) { return str; } アロー関数 const func2 = (str) =>{ return str; } 分割代入 オブジェクト const ob = { name: "hoge", age: "30", }; const { name, age } = ob; const message = `私は${name}です。${age}歳です。` 配列にも使える! 配列の場合は、名前が決まってないから順番でうけとる。 const array_ = [ "hoge", 30 ]; const [ name, age ] = array_; const message = `私は${name}です。${age}歳です。` デフォルト値 関数の引数を空欄にしてもデフォルト値が設定されているので、"Hello, who!"と出力される。 const sayHello = ( name = "who" ) => console.log(`Hello, ${name}!!`); sayHello(); スプレッド構文 配列の展開 配列の中身を順番に開くみたいなイメージ。 const arr = [1, 2]; const sumFunc = (num1, num2) => console.log(num1 + num2); sumFunc(arr[0], arr[1]); sumFunc(...arr); まとめる 配列のコピー、結合 map, filterの使い方 論理演算子の意味 exportの書き方
- 投稿日:2022-02-24T18:50:47+09:00
JS の every メソッドと min/max 関数
JavaScript の every メソッド 少し前に、 JavaScript のメソッドについて話題になっていました。 Array.prototype.every() この every メソッドは、他の言語だと any とか forall とかの名前で実装されている関数・メソッドとほぼ同じもので、「その配列の中の全ての要素が条件を満たすか」を判定し真偽値で応えてくれるものです。 every メソッドを、空配列をレシーバにして呼び出した時に何が起こるか、という観点が注目されていました。これは、常に真を返します。たとえ恒偽式が与えられていようとも、です。 [].every(_ => false); //=> true このような挙動について、動きが分かりやすいかどうか、理に適っているかどうか、納得できるかどうか、という点が議論の焦点になっていたように思います。 私個人としては、この挙動は分かりやすく、理に適っていて、納得できるものに思えました。 数学的観点からの every メソッド 数学的に見ると、このメソッドは「全ての」( for all )つまり ∀ 記号と同じ意味です。 そして、空集合に対して「全ての要素が任意の条件を満たしているか」という判定は必ず真になります。 ……この説明、上記の MDN の解説ページにもちゃんと書いてありました(私は空虚に真という用語は知りませんでした)。 つまり every メソッドが空配列に対して常に真を返すのは、数学的には理に適っているし納得できるものだということです。 畳み込みとしての every メソッド every メソッドは「集合をブール論理、単位元を true 、二項演算を論理積とするモノイドに対する畳み込みである」と解説している方もいました。 詳しい説明は避けますが、これは要するに「最初に単位元(この場合は true )を用意し、コレクションから要素を次々と取り出して与えられた関数に適用した後、その結果と前の結果との二項演算(この場合は論理積)を行う」ということです。 ……と言葉で書いても何だかよく分らないので、コードで書きましょう。畳み込みは、 fold とか reduce という名前の関数・メソッドで多くの言語に実装されており、 JavaScript にもあります。 [1, 10, 100].reduce((res, _) => res && false, true); //=> false [].reduce((res, _) => res && false, true); //=> true 畳み込みを空のコレクションに対して適用すると、初期値がそのまま返って来ます。 every メソッドを畳み込みとして見た場合、初期値は true なので、空配列に対して呼び出せば true が返ってくるのは当然だ、という話ですね。 他の言語の似たような機能の挙動 参考として他の言語の挙動を見て見ましょう。 と言っても網羅的なものでは全く無く、私が普段触っている幾つかの言語でどういう挙動をするか見てみた程度です。 Ruby [1,2,3].all? { false } #=> false [].all? { false } #=> true Scala Seq(1,2,3).forall(_ => false) //=> false Seq().forall(_ => false) //=> true OCaml List.for_all (fun _ -> false) [1; 2; 3];; (* false *) List.for_all (fun _ -> false) [];; (* true *) Haskell all (\_ -> False) [1, 2, 3] -- False all (\_ -> False) [] -- True ……正直ちょっと言語が偏りすぎていてあんまり参考にできるものかどうかは自信が無いのですが、いずれの言語でも JavaScript と同様の挙動をしていることがわかります。 私は上に挙げた言語を普段使っているので、それと同じ挙動をする JavaScript の every メソッドは自然に受け入れることができた、という話ですね。 every メソッドまとめ というわけで、数学的観点からも、畳み込みという観点からも、他の言語との比較に於いても、 JavaScript の every メソッドの挙動はとても自然なものである、という説明ができたかと思います。 ……ここまでが本記事の 前座 となります。 JavaScript の min max 関数 上の話題から派生して、「 JS は min max 関数の挙動も面白い」みたいな話が流れてきていました。 Math.min() Math.max() 動きを見てみましょうか。 Math.min(1,2,3); // もちろん 1 が返る Math.min(); // Infinity が返る……!? Math.max(1,2,3) // もちろん 3 が返る Math.max(); // -Infinity が返る……!? この挙動に関して、私は違和感を覚えました。言い換えると、「あまり嬉しい挙動ではない」と感じました。 しかしどうしてそう感じてしまったのでしょうか? というのを、ここから説明していきたいと思います。 畳み込みとしての min max 関数 この JavaScript の min max 関数の挙動は、上の every で行ったように畳み込みとして説明ができるという解説がありました。 同じことを2度書く必要は無いので、以下では min の場合にのみ限って話を進めていきましょう。 min 関数の挙動を畳み込みとして書くと、以下のようになるでしょう。 [1,2,3].reduce((res, elm) => res < elm ? res : elm, Infinity); この時、 every メソッドの解説で使った「二項演算」と「単位元」を考えてみます。 「二項演算」は、「2つの値のうち、小さい方を返す」という関数になります。 「単位元」は、他のどの数と一緒に上の二項演算に適用しても相手の数を返す値なので、 JavaScript の number の中では Infinity がこれに当たります。 さて、空のコレクションに対して畳み込みを行った場合、単位元を返すのでしたね。 よって、 Math.min() 関数を引数無しで呼び出した時には Infinity が返るのは当然、という話です。 ……確かに、畳み込みとして見ると min 関数の挙動はとても自然なものに思えます。 「どのようにするか」より「何をするか」 ところで、 min 関数は「畳み込み」なのでしょうか? 今まで散々そういう説明をしてきたじゃないかという話なのですが、これは「そういう実装になっている」という話であって、関数の使い手が、それを畳み込みであると認識している、あるいはする必要があるという主張とは区別できるでしょう。 プログラミング言語で処理を関数にまとめることの利点に、その処理を抽象化できるという点があります。 何か大きなプログラムを作る際に、本質的ではない処理の実装に思考力を取られることを避けて、実際に行いたい処理にだけ集中できるという利点ですね。 よく「関数の説明には『どうやったか』ではなくて『何をするのか』を書け」とか、「 How ではなく What 」とか言われているやつです。 一度、内部実装の話を頭から追い出して、素朴な使い手として min 関数を見て見ましょう。 我々はこの関数を「2つの要素から小さい方を返す畳み込み」として使いたいのでしょうか? もっと単純に、 このコレクションの中から一番小さい要素を返す処理 として使いたいのではないのでしょうか? 数学的観点からの min max 関数 ※この項で使ってる用語とかの定義はこの辺りを参考にしていますが、何分数学には疎いので、何か間違いがあったらごめんなさい 大抵の関数の使い手は、おそらく内部でどのような処理を行っているかには興味が無く、この関数を「コレクションの中の一番小さな要素を返してくれる」ものとして利用しているでしょう。 (勿論、パフォーマンスを求めてカリカリにチューニングしたい場合とかは別でしょう。競プロ用途とか。で、それは「大抵の関数の使い手」から外れていますよね?) 具体的な動きではなくて、もっとふわっとした意味で使っていることが多いと思います。 ただ、定義がふわっとしたままだと扱いにくいので、 every の時でも行ったように数学的な定義を援用して話を進めてみましょう。 数学的に見ると、 min 関数は (半順序?)集合の最小元を求める 操作と言えるでしょう。 「一番小さな要素を求める」という素朴な感覚と、そんなに離れていない処理だと思います。 では、「 Math.min() 関数を空引数で呼び出す」に相当する操作は何でしょうか? これは、 空集合の最小元を求める ことになるでしょう。 なるほど。一体何になるのでしょうか。 ……いろいろ調べたのですが、よく分りませんでした。 いかがでしたか? 何故分からないかというと、そもそも「集合の最小元」というのが 空ではない集合に対して 定義してあるものだからですね。 「最小元を求める処理」には「空集合でない場合に限る」という注釈が付くわけです。 前提条件を満たしていないわけですね。 こういう場合、「最小元は存在しない」あるいは「操作が未定義」とでも言えばいいのでしょうか。 つまり、数学的に言えばおそらく、「 Math.min() 関数を空引数で呼び出す」という処理は許されていないわけです。 他の言語との比較 許されていない処理を行った場合に何が起こるのか。 これは一種の未定義動作ですね。 未定義動作なので、 JavaScript の関数がどんな値を返しても文句を言われる筋合いはないのかもしれません。 それを踏まえた上で、では他の言語ではどのような挙動をするのだろうか、という点を確認してみましょう。 またしても私が常用している言語を適当に見てみただけなので、何ら説得力を持つ調査結果ではないのですが……。 Ruby [1,2,3].min #=> 1 [].min #=> nil Scala Seq(1,2,3).min //=> 1 Seq.empty[Int].min //=> java.lang.UnsupportedOperationException: empty.min Seq.empty[Int].minOption //=> None Python # 普段使わないのだけど、気になったので調べてみた min(1,2,3) #=> 1 min() #=> TypeError: min expected 1 argument, got 0 min([1,2,3]) #=> 1 min([]) #=> ValueError: min() arg is an empty sequence Java // こっちも普段は使ってないけど調べてみた Collections.min(Arrays.asList(1, 2, 3)); //=> 1 Collections.min(new ArrayList<Integer>()); //=> java.util.NoSuchElementException Haskell minimum [1,2,3] -- 1 minimum [] -- Exception: Prelude.minimum: empty list ※ OCaml の標準ライブラリにはコレクションの最大値・最小値を求める関数は無さそうでした。 これを見ると、「存在しないことを表す値を返す( Ruby, Scala の minOption )」か、シンプルに「例外を投げる」( Python, Java, Haskell, Scala の min )かという挙動になっていますね。 どうして私は違和感を覚えたのか 上の他言語との比較が分かりやすいのですが、私が普段使っている言語だとこういう場合に、空の値を返すにしろ例外を投げるにしろ、正常ではない挙動をするのですね。例えば Ruby は型が厳格な言語なので、数値系の演算に nil を引数として与えると大抵の場合は例外が飛びます。 そういう言語に慣れていたために、 Infinity という 数値として意味のある値 が返ってくる仕様に驚きを感じ、知らない物を排除する本能から「好ましくない」と感じたのだと思われます。 個人的には、空配列の最小値を求めるような処理を行うようなことはまず無く(そもそもそんな値は存在しないのですから)、それを行おうとした段階で何かが間違っているので、空の値を返すなりさっさとエラーを吐いてくれるなりした方が使い勝手が良いなぁと感じます。 余談 ここからは完全に余談です。 本文以上にテキトーなことを言っているので気をつけてください。 最小元、極小元、下限、下界 どれも定義が違います。数学むつかしい……。 記事中で「空集合の最小元は存在しない」と書きましたが、実は下限は存在します。 https://en.wikipedia.org/wiki/Infimum_and_supremum ( Wikipedia の記事ですが典拠が付いています。) 空集合に対する下限は ∞ であり、また上限は -∞ ですね。 JavaScript の Math.min() 関数は下限を得る関数だった……? (下限は infimum なので関数名が min なのと整合性が取れないのですが……。) min max は二項演算? 記事中では min max 関数を畳み込み演算として扱いましたが、 そもそも min max は二項演算では? という方もいらっしゃるかもしれません。 min を畳み込みとした際のモノイドの二項演算を「2つの値のうち、小さい方を返す演算」としましたが、これこそがまさに min 関数だろ、という話ですね。 実際、可変長引数を許さない Haskell や OCaml では、 min max 関数は厳密に2引数を取る関数であり、型としても閉じているので、二項演算です(……よね?)。 Haskell 42 `min` 99 -- 42 42 `max` 99 -- 99 確かに、二項演算とそれを使った畳み込みは別物で、別名が付いている場合も多いかもしれません。 例に挙げた Haskell では、二項演算としての min 関数と、そのコレクションに対する畳み込みとしての minimum 関数がそれぞれ用意されているわけです。 一方で Lisp なんかだと、二項演算の関数を畳み込みとしても使えることがあるような気がします。 例えば台集合を整数、二項演算子を + 、単位元を 0 とした時、畳み込みは sum 関数などとして定義されていることが多いですが、 Lisp 系の言語だと + 関数がそのまま sum として働きます。 Lisp (+ 1 2) ;=> 3 (+ 1 2 3 4) ;=> 10 なので、「二項演算子的な関数を可変長引数にすると畳み込みになる」みたいな考え方は割とあるのかなぁ、と考えるなどしました。 可変長引数? コレクション? 他言語の例に出した Ruby Scala Haskell の min ( Haskell は minimum )メソッド・関数は配列やリストのようなコレクションを取るのに対して、 JavaScript の Math.min() は可変長引数ですよね。 これを混同しても良いものかどうか少し悩んだのですが、何かご指摘があった際に考えることとします……( Ruby も Scala も関数じゃなくてコレクション自体のメソッドだし、仮に関数だとしてもこいつらはコレクションをメソッドの可変長引数として渡せるし、そんでもって受け取る時はコレクションになってるし、 Haskell はそもそも可変長引数が無いし、この辺の差異は今回の話題とはあんまり関係無い所だと思うけどそういう部分に突っ込み飛んできたらうーん…… 謝辞 Slack 等で知人のエンジニアの方々に大いに助けていただきました。 ありがとうございます
- 投稿日:2022-02-24T18:47:03+09:00
ブラウザバック判別
if(window.performance.navigation.type === 2) { // ちなみにリロードの場合は1 // ブラウザバッグ時に実行したいイベント } こっちでもいける if(window.performance.navigation.type === performance.navigation.TYPE_BACK_FORWARD){ // リロードの場合はTYPE_RELOAD // ブラウザバッグ時に実行したいイベント }
- 投稿日:2022-02-24T18:23:47+09:00
touchやwheelのイベントリスナーにpassive: trueを明示的に指定する必要があるのか調べてみた
これは何 EventTarget.addEventListener() #パッシブリスナーによるスクロールの性能改善 - Web API | MDN にあるように、以下のようなイベントに対してイベントリスナーを設定すると、スクロールの処理性能が大幅に低下する可能性があります。 touchstart touchmove wheel mousewheel それに対して、passiveオプションをtrueで設定するとこの問題を防ぐことができるようですが、一部のブラウザではデフォルトでpassive: trueになっているということで、毎回明示的に設定する必要があるのかどうか調べてみました。 結論 ChromeやFirefoxではデフォルトでpassive: trueになっている (window, document, document.bodyが対象のとき) EventTarget.addEventListener() # ブラウザーの互換性 - Web API | MDN MDNの互換性の表ではNoになっているmacOS Safari(15.3)でも touchイベント: Safari 11.1でデフォルトpassiveになっていそう (実機で試してはいない) Safari 11.1 wheelイベント: 「実験的な機能」の中に、Wheel Event listeners on the root made passiveがあり、デフォルトオンになっている React17でイベントハンドラを設定する場合もデフォルトでpassive: trueになっている Keep onTouchStart, onTouchMove, and onWheel passive by gaearon · Pull Request #19654 · facebook/react ただし、逆にpassive: trueにする方法がないため、これらのイベントに対してe.preventDefault()をすることができない React 18 not passive wheel / touch event listeners support · Issue #22794 · facebook/react という前提で必要なときに指定する passive: trueについて touchやwheelイベントはイベントリスナーを設定すると、スクロールをする前にスクロールをブロックする処理がないかを確認するため、非同期のスクロールがブロックされてスクロールがカタつくという、いわゆるScroll Jankが発生します。1 それを防ぐために、スクロールをブロックする処理 (e.preventDefailt()) がない場合はpassive: trueというオプションをイベントリスナー設定時に付与してあげることでScroll Jankを防ぎ、スクロールをスムーズに行うことができるようになります。 ブラウザデフォルトの挙動について 実はこのtouchやwheelの一部イベントのpassive: trueオプションですが、ChromeやFirefoxではデフォルトでpassive: trueになっています。2 (対象はwindow, document, document.body3) しかしMDNの互換性の表を見てみると、Safariはデフォルトがtrueになっていないようです。 Safariでの挙動について macOS Safari version: 15.3 実際に以下のようなコードを実装し、Safariで動作確認をしてみます。 document.addEventListener('wheel', (e) => { e.preventDefault() console.log('handler called') }); 確認をしてみると、スクロールは無効化されず、普通にスクロールができてしまいました。 次に明示的にpassive: falseを渡してみます。 document.addEventListener('wheel', (e) => { e.preventDefault() console.log('handler called') }, { passive: false }); すると予想通りスクロールを無効化することができました。 実はSafariの「実験的な機能」の中に、Wheel Event listeners on the root made passiveがあり、オンになっていることでこのような挙動になっていたようでした。これはSafari15.3ではデフォルトでオンのようです。 (いつからこうなっているのかまでは調べきれませんでした) 試しにオフにしてみると、最初のコードでもスクロールが無効化されました。 また、touchイベントも、Safari 11.1からデフォルトでpassive: trueになっているようです。4 Reactでの挙動 React 17ではこちらのissueでデフォルトの挙動について議論がされた後、passive: trueがデフォルトになったようです。 対象のイベントハンドラは、onTouchStart, onTouchMove, onWheelです。5 ただ、passive: trueになったのはいいのですが、現時点(react@17.0.2)ではこのpassiveを操作できるオプションが存在していないようで、逆にpassive: trueにすることができない問題もあるようです。これにより、onTouchStart, onTouchMove, onWheelのイベントハンドラ内ではe.preventDefault()でイベントをブロックできないようです。 const handler = (e) => { e.preventDefault() // スクロールを止めることはできない console.log('handler called') } const App = () => { return ( <div onWheel={e => handler(e)}> <Contents /> </div> ); } これについては、issueも存在している(React 18 not passive wheel / touch event listeners support · Issue #22794 · facebook/react)ようなので、今後対応されるかもしれません。 まとめ ChromeやFirefoxではデフォルトでpassive: trueになっている (window, document, document.bodyが対象のとき) EventTarget.addEventListener() # ブラウザーの互換性 - Web API | MDN MDNの互換性の表ではNoになっているmacOS Safari(15.3)でも touchイベント: Safari 11.1でデフォルトpassiveになっていそう (実機で試してはいない) Safari 11.1 wheelイベント: 「実験的な機能」の中に、Wheel Event listeners on the root made passiveがあり、デフォルトオンになっている React17でイベントハンドラを設定する場合もデフォルトでpassive: trueになっている Keep onTouchStart, onTouchMove, and onWheel passive by gaearon · Pull Request #19654 · facebook/react ただし、逆にpassive: trueにする方法がないため、これらのイベントに対してe.preventDefault()をすることができない React 18 not passive wheel / touch event listeners support · Issue #22794 · facebook/react という前提で必要なときに指定する 参考 MDN EventTarget.addEventListener() #パッシブリスナーによるスクロールの性能改善 Chrome Making touch scrolling fast by default | Web | Google Developers Safari Safari 11.1 218842 – Force wheel event listeners on the root to be passive React Touch/Wheel Event Passiveness in React 17 · Issue #19651 · facebook/react Keep onTouchStart, onTouchMove, and onWheel passive by gaearon · Pull Request #19654 · facebook/react React 18 not passive wheel / touch event listeners support · Issue #22794 · facebook/react https://dom.spec.whatwg.org/#observing-event-listeners ↩ https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%83%BC%E3%81%AE%E4%BA%92%E6%8F%9B%E6%80%A7 ↩ https://developers.google.com/web/updates/2017/01/scrolling-intervention ↩ https://developer.apple.com/library/archive/releasenotes/General/WhatsNewInSafari/Articles/Safari_11_1.html ↩ https://github.com/facebook/react/pull/19654/files ↩
- 投稿日:2022-02-24T17:37:18+09:00
【個人開発】日々の晩酌を記録する日記帳「お酒録」をリリースしました
はじめに・・・ これまでに飲んだお酒を覚えていますか?色は?味は?フレーバーは? そうした日々のお酒を記録するためのWebサービス「お酒録」を作りました。 自分の飲んだお酒を記録することはもちろん、他の人が飲んでいるお酒を見たり、コメントできます。 ワインや日本酒だけに特化した類似のスマホアプリはありますが、 本サービスはそうした区別をせず、ビール、酎ハイ、日本酒、ワイン、焼酎、ウィスキーとあらゆるお酒を対象としています。 また、Twitterにお酒の写真を投稿する人が多いことからTwitterへの投稿機能も備えています。 お酒はビールや日本酒といった種類だけじゃなく、銘柄にも興味を持つとより楽しめるものと思います。 この香りが〜とか、この味が〜といった感じで。 そんな観点で、これまでお酒の銘柄を気にしたことがなかった人ほど使って欲しいです。 ※お酒は20歳になってからです。飲み過ぎにご注意ください。 「お酒録」3つの機能 記録する 日々飲んでいるお酒を記録し、後から見返すことができます。 いわゆる、「お酒の日記帳」としての使い方です。 お酒の銘柄はもちろん、味わいや香りも残すことができます。 上記の日々の記録に加えて、下のように統計データを表示することもできます。 探す 他の人のレビューを見て、自分が次に購入するお酒を探すことができます。 ユーザーの皆さまの投稿を元に夫々の銘柄ごとにフレーバーやスッキリしている等の味覚チャートが表示されるようになっています。 繋がる お酒録にTwitterアカウントを登録することで、お酒録からTwitterアカウントへ、また、お酒録に投稿した内容をTwitterにも投稿することで、お酒好き同士の方々が繋がる仕組みを作っています。 開発環境 今回は、Vue.jsを採用して、SPA(シングルページアプリケーション)として作成しました。 また、UI周りはVuetify、データベースや認証機能関連はとっつきやすいFirebaseを採用しました。 使用言語 Vue.js(SPAとして開発) ライブラリ Vuetify Vue-Router Browser-Image-Compression(投稿画像圧縮のため) ##その他 Twitter API Firebase 所感 SPA(シングルページアプリケーション)も捨てたもんじゃない 今回はSPAとして作成しました。 ネットでSPAについて調べると、SEO対策で不利とか実用レベルではない!と言われています。 まぁ、確かにSSRで作成するに越したことはないと思います。 ですが、SPAも昔に比べると大分受け入れられる下地ができてきたように思います。 Google検索にも引っかかるしAdsenseの広告審査にも通る時代になりました。 とはいってもSSRには敵わないな〜というのが私の感想です。 この「お酒録」はSPAに固執してしまいましたが、今後、一部はSSRに切り替えようと思っています。 普段コーディングばっかりやってる人はデザインを学んだ方が良いかも 普段はコードを書いていることの方が多く、デザインをゼロからやったのは今回が初めてです。 今更ですが、デザインは難しいですね。。。 今回の「お酒録」は、全体のデザインを3、4回はゼロから作り直すハメになりましたw 個人開発だからと甘えずにデザインは最初の段階で各機能も含めて、しっかりとデザインを最初に作った方が開発速度はかなり速くなるだろうと思います。
- 投稿日:2022-02-24T17:10:40+09:00
JavaScriptのコールバック関数を理解する
コールバック関数とは コールバック関数とは、関数またはクラスメソッドの引数として関数を渡すことで、実行する処理を外部から注入するための手法です。 JavaScriptにおいて、このコールバック関数は非常に重要な役割を持ちます。 いたるところでコールバック関数が散見され、コールバック関数を見ないJavaScriptプロジェクトはないのでは? と言われるレベルです。 コールバック関数の仕組み コールバック関数は、前述のとおり関数の引数として関数を渡すということをしています。 C言語で言う所の「関数ポインタ」なのですが、なかなか理解されないことが多いです コールバック関数の非常に簡単な例が以下のコードです。 // コールバックを受け取る関数 function loopback(_callback) { console.log('処理開始'); // 1~5までの数字をコールバック関数に渡す for(let i=1;i<=5;i++) { _callback(i); } console.log('処理終了'); } // コールバック関数を指定して、「loopback」を呼び出し loopback(function(i) { console.log(`callback number is ${i}`); }); これを実行すると、以下のようにコンソールに出力されます。 処理開始 callback number is 1 callback number is 2 callback number is 3 callback number is 4 callback number is 5 処理終了 コールバック関数が内部でどう使われているか ここでのポイントが、関数「loopback」の呼び出しに対して、関数を指定しているという点です。 関数「loopback」上では、普通の変数と同じように引数として関数を受け取っています。 function loopback(_callback) { そのうえで、この関数は、あたかも定義された関数かのように利用できます。 for(let i=1;i<=5;i++) { _callback(i); } このように、JavaScriptは関数であっても変数として取り扱うことができるという珍しい特性を持っています。 (と言っているとC言語使いに怒られます) そのため、以下のように呼び出す部分を書き換えると理解がしやすいかもしれません。 変更前 loopback(function(i) { console.log(`callback number is ${i}`); }); ↓ 変更後 const callback = function(i) { console.log(`callback number is ${i}`); }; loopback(callback); コールバック関数の引数 今回のサンプルでは、コールバック関数の引数として数値を渡しています。 このように、この引数として渡される値はコールバックごとにあらかじめ決められています。 そのため、「コールバック関数だから引数は○○だ!」といったような決まりごとはなく、 コールバックに引数がない場合もあります。 コールバック関数を要求するような機能を利用したい場合には、あらかじめ必要な(受け取れる引数の数と型)をしっかりと確認するとよいでしょう。 さいごに コールバック関数はJavaScriptを使う上で非常に重要です。(大事なので2回) いわゆる「コールバック地獄」なんていう単語も存在するくらいなので、気になる方は調べてみるとよいでしょう。
- 投稿日:2022-02-24T16:59:55+09:00
Jamstack&Nature:Charm Industrialによる二酸化炭素排出量の削減
何年もの間、人間は環境を利用し、環境が我々に与えてくれたものを乱用しました。そして、環境を危険にさらすまでに至りました。 著者:Magda T. 2021年10月14日 原文:https://bejamas.io/blog/carbon-removal-case-study/ 私たちは自然に深くお世話になっており、自然を保護するために全力を尽くさなければならないことは周知の事実です。「大気中のCO₂を永久に除去する」というCharm Industrialの使命は、まさにそこにあります。 Jamstackのコミュニティは、テクノロジーをより持続可能な方法で使用することにも力を入れています。そのため、Bejamasのチームは、二酸化炭素除去のサブスクリプションをより多くのお客様に知らせ、購入を促すという目標を掲げているCharm Industrialと提携する機会に飛びつきました。 1. Charm Industrialの背景 2018年、エンジニアのグループが力を合わせて、CO₂の完全除去によって何世代にもわたる環境破壊を巻き戻すという、難しくも重要な課題に着手し始めました。 Peter Reinhardt、Kelly Hering、Shaun Meehanの3人は、サンフランシスコでCharm Industrial社を設立しました。 その背景にある科学に少し触れてみましょうか。 エンジニアとしての知識を活かして、廃棄物であるバイオマスを高速熱分解と呼ばれるプロセスでバイオオイルに変換することに挑戦しました。 2. JamstackでCO₂の除去を促進する 彼らのチームが、オンラインでの購入を可能にする、クリーンで信頼性の高い、使いやすいウェブサイトを求めて私たちに連絡してきたとき、Jamstackがどのように貢献できるかを明確にイメージできました。 「未来のためにより良い世界を作る」というミッションに賛同した私たちのチームは、彼らの要望を実現するために以下の仕事に取り掛かりました。 ・新しいホームページの構築 ・支払いプロセスの自動化 ・企業のバイオマス購入を促進するためのデザイン変更 バイオオイルに変換された後は、ネガティブ・エミッションとして地下深くに注入したり、改質してグリーン水素を生成したりして、燃料や工業化学品として利用します。 一例として、この技術はShopifyのための1000トンのCO₂削減に貢献しました。 しかし、彼らにはもう一つの課題がありました。それは、製品を宣伝し、顧客に販売するためのより良い方法を見つけることでした。 3. Charm Industrial社のJamstackウェブサイトの構築 彼らのウェブサイトの最大のポイントは、耐光性でかつ目を引くデザインで、お客様に情報を提供し、製品の購入を促すものでなければなりませんでした。 そのため、ヘッドレスCMSであるJamstackのアプローチを用いて、見た目にも美しく、Charm Industrialからの購入を検討している人に必要な情報がすべて含まれた静的なサイトを作成しました。 3-1 スタック 私たちのスタックには、以下のような素晴らしいツールが含まれていました。 3-1-1 Gatsby.js シンプルさを考慮して、Gatsbyを選択しました。これにより、React、GraphQL、Webpackを使ってどこにでもある機能を組み合わせることができ、痛みや労力がない静的なウェブサイトを構築することができました。 3-1-2 STORYBLOCK CMS 可能な限りサステイナブルなサイトにするために、最も根拠のある方法で高速なサイトを構築したかったので、ヘッドレスのコンテンツ管理システムとしてStoryblokを使用しました。Storyblokを採用したことで、Charmチームはコードを必要とせずに迅速に編集を行うことができました。 3-1-3 TailwindCSS TailwindCSSは、実用性と表示性を重視したCSSフレームワークです。Charmの場合は、カラーパレットやタイポグラフィに統一感があり、見栄えのするサイトであることが必要不可欠でした。また、CSSのバンドルをできるだけ小さくすることで、環境に優しいという共通の目標も達成しています。 依頼内容は、単純化と自動化というシンプルなもので、時間が最重要視されていました。この次は、支払いプロセスの自動化です。 4. 支払いプロセスの自動化 ここでは、Charm Industrialとの仕事の中で、最も重要と言える部分を紹介します。彼らは手動で購入を処理していましたが、これはARRを100万ドルにするという野心的な目標を持つビジネスにとって現実的ではありません。 私たちのチームはすぐに、Stripeを使った信頼性の高い、迅速な自動決済システムの導入に取り掛かりました。これにより、お客様の購入プロセスが非常にシンプルになっただけでなく、ウェブサイトでの購入に対する信頼感が増し、以前の支払い方法にあった手作業をすべて取り除くという目標を達成することができました。 フロントリードの開発者であるDamian Rogulskiはこう話しています。 StripeにはStripe Checkoutという非常に開発期間の短い機能があり、それほど手間をかけずにチェックアウトを作成することができます。また、支払いを設定するための多くのオプションを備えた優れたダッシュボードと、すべての支払いトランザクションを管理するための管理者用の強力なダッシュボードも備えています。 Damian Rogulski Bejamasのフロントエンドリード開発者 4-1 企業のバイオマス購入意欲を高めるために、デザインを一新 これらの結果、美しさと速さだけではなく、仕事もできる洗練されたウェブサイトをお届けすることができました。 Charm Industrial社のIlya Volodarsky氏のコメントです。 私たちは、Charm Industrial社の新しいウェブサイトをBejamas社と共同で構築し、消費者や中小企業がカーボン除去の月額プランを作成できるようにしました。彼らのチームは、一緒に仕事をするのが楽しく、コミュニケーションが明確で、Charmのミッションに惚れ込んでくれました。その結果がサイトによく表れています。期限内に新しいサイトを納品してくれただけでなく、サイトは驚くほど安定しており、素晴らしいUXを備えていました。数ヶ月の間に、信じられないほどの高速反復を達成することができたので、すべての機能を改善し、時間通りに出荷することができました。 Ilya Volodarsky Charm Industrialの共同創設者、Segmentの共同創設者 5. Charm Industrialのミッションへの貢献 地球を守ることは、Bejamasの基本的な価値観のひとつであり、このプロジェクトは私たちの思想にとても近いものでした。 二酸化炭素排出量の削減を通じて持続可能性を促進する方法として、このサイト制作を成功でき、また、彼らのミッションの一部になれたことを嬉しく思います。 私たちは行動を起こし、CO₂除去の定期購入をできるようにしたことを誇りに思います。 最後まで読んで下さり、ありがとうございました Jamstackに関心がある方はこちらまでお問合せください! 株式会社ヒューマンサイエンス https://www.science.co.jp/document/jamstack.html
- 投稿日:2022-02-24T16:44:57+09:00
JavaScript Canvasのサイズについて
HTMLコード <body> <canvas id="can"></canvas> <script src="example.js"></script> </body> 上記コードのように、HTMLで「canvas要素」をID名「can」で記述し、 それをJavascript(ファイル名「example.js」)でいじっていきたいときの サイズに関してのお話です。 まず「canvas」には「キャンバスサイズ」と「表示サイズ」があり、 「キャンバスサイズ」はHTMLで定義したcanvas要素をJavaScriptでいじっていく時の 基本となるサイズになります。 「表示サイズ」は、「キャンバスサイズ」で決定したサイズや形などを基準にどれだけの倍率でブラウザに表示するかというサイズになります。 JSコード let can=document.getElementById("can"); let con=can.getContext("2d"); can.style.border="5px solid #000"; function square(){ con.fillStyle="red"; con.fillRect(140,65,20,20); } square(); 上記コードのように、 まず「getElementById」でHTMLの「canvas要素」を取得、 「getContext」で2Dの描画を作成していくとし、 キャンバスサイズ(初期値)に枠線を描画、 塗りつぶし(fill)の「色」と「座標、サイズ」を指定した「square関数」を定義、 その「square関数」を呼び出し、 上記画像をブラウザに表示させました。 まず最初に知っておかないといけないのは キャンバスサイズは何も設定していなければ 初期値である「width:300px,height:150px」に自動で設定されます。 なので画像のような横長長方形になっているんですね。 キャンバスサイズは初期値の300×150ですが、 表示サイズもまだ何も設定されていないため、 キャンバスサイズに準じている状態です。 その初期値の縦横の長さを踏まえた上で、「square関数」で指定した塗りつぶし四角を 丁度真ん中に持ってくるように指定しています。 それではこの長方形のキャンバスサイズ正方形にするために、 まずキャンバスサイズを変更していきます。 HTMLコード <body> <canvas id="can" width="300px" height="300px"></canvas> <script src="example.js"></script> </body> HTMLのcanvas要素に上記コードのように「width」と「height」を追加して指定すると 初期値であった長方形が、 画像のような正方形のキャンバスサイズに変更されました。 次に「表示サイズ」を見ていきます。 表示サイズは今まで設定してきた「キャンバス」自体をブラウザにどのように表示させるかというもので、 普通はCSSを使って記述していくのですが、 今回はJSにCSSを記述していくやり方です。 JSコード can.style.width="300px"; can.style.height="300px"; 先程のJSの記述に上記コードを追加しました。 JSにサイズのCSSを記述するときは、 取得ID名(今回だと「can」).style.「width or height」="サイズ"; という感じで記述します。 このコードだと、先程設定した「キャンバスサイズ」も300×300なので、 表示サイズもキャンバスサイズと一緒になり、先程の正方形になります。 JSコード can.style.width="600px"; can.style.height="300px"; 次に表示サイズのwidthを600pxにしてみます。 すると、黒の外枠は横長の長方形になりましたが、中の赤正方形も横に引き伸ばされました。 これは元の「キャンバスサイズ」は変わらず300×300の正方形なのに対し、 「表示サイズ」を横長にすることによって キャンバス自体はそのままに、表示サイズで設定した横幅600pxまで引き伸ばした結果になるためです。 HTMLで「canvas要素」を設定して、 「キャンバスサイズ」を設定せず(この時点で縦150px、横300pxの初期値)、 CSSで表示サイズを300×300の正方形に設定したとしても、 上記画像のようにブラウザに表示されるのは 横長のキャンバスが縦に引き伸ばされた正方形ができてしまうということですね。
- 投稿日:2022-02-24T16:19:26+09:00
javascript での位置取得と submit の備忘録
はじめに 仕事で位置情報を取得する Javascript コードを書いたのでその際の経験を忘れないように記載しておきます。 位置情報取得の構文 以下の構文が現在地を取得する基本的な構文です。 function success(position) { // 成功の場合 var latitude = position.coords.latitude; // 緯度 var longitude = position.coords.longitude; // 経度 }; function error() { // エラーの場合 alert("現在位置を取得できません"); }; // 位置情報を取得し、どちらかのコールバック関数を実行 navigator.geolocation.getCurrentPosition(success, error); Geolocation.getCurrentPosition() navigator.geolocation.getCurrentPosition(success, error) は端末の現在位置を取得するメソッドです。位置情報の取得に成功した場合は success に与えたコールバック関数を実行し、位置情報の取得に失敗した場合やユーザーが位置情報の取得を拒否した場合は error に与えたコールバック関数を実行します。 その他、第3引数にオプションの設定が可能です。 コールバック関数の入力引数 getCurrentPosition(success, error) のうち、success 側には GeolocationPosition インターフェイスのインスタンスが与えられ、現在位置の情報や取得した時刻が取得できます。 error 側には GeolocationPositionError インターフェイスのインスタンスが与えられ、エラーコードやエラーメッセージを取得できます。 Geolocation.getCurrentPosition() と submit() の併用 ここで、位置情報の取得と submit が並んだ際の挙動について記載しておきます。 問題設定 位置情報の取得と submit() の併用のサンプルとして、 送信するボタンを押下した際に実行される 現在位置を取得して、指定の input に値を設定 設定後にフォームの内容を submit という処理を行う pushSubmit() 関数を用意するとします。 実際に書いた構文と発生した問題 例えば、以下の様な構文を書いたとします。 function pushSubmit() { function success(position) { // 成功の場合 var latitude = position.coords.latitude; // 緯度 var longitude = position.coords.longitude; // 経度 document.getElementById('latitude').value = latitude; document.getElementById('longitude').value = longitude; }; function error() { // エラーの場合 alert("現在位置を取得できません"); }; // 位置情報の取得を試みる navigator.geolocation.getCurrentPosition(success, error); // 実際にsubmit var form = document.getElementById('form'); form.submit(); } この構文は流れとしては 「位置情報の取得 → submit」 と書いているので位置情報が取得できているように思われます。 ですが、実際に実行して submit されたサーバサイドでリクエストパラメータを確認すると以下の様になります。 [ 'latitude' => null, 'longitude' => null ] ご覧の通り、取得したはずの位置情報が記載されていません。 これはコードの実行とコールバック関数の処理の順序が原因です。 getCurrentPosition() が実行され、その結果を基に success のコールバック関数が実行される場合でも、successに処理が渡った段階でメインの処理もsubmit()に進むため、successの処理が終わる前に submit されてしまうという流れになります。 対策 対策は簡単で、コールバック関数の中に submit を移動してしまうというものです。 function pushSubmit() { function success(position) { // 成功の場合 var latitude = position.coords.latitude; // 緯度 var longitude = position.coords.longitude; // 経度 document.getElementById('latitude').value = latitude; document.getElementById('longitude').value = longitude; // 実際にsubmit // var form = document.getElementById('form'); // <-- ここに移動 form.submit(); // }; function error() { // エラーの場合 alert("現在位置を取得できません"); }; // 位置情報の取得を試みる navigator.geolocation.getCurrentPosition(success, error); } 上記の様に構文を変更することで、確実に指定の input に値を設定した上で submit されます。 もし、位置情報の取得が失敗した場合も submit したい場合は error() にも submit の構文を記載してください。
- 投稿日:2022-02-24T15:52:20+09:00
Express で レスポンスに "Content-Type" ヘッダを自前セットする方法どれを選べばいいの
概要 Express 4 で、HTTPレスポンスヘッダーに "Content-Type" をマニュアルでセットする方法のハンズオン記録です "Content-Type" ヘッダをセットする書き方はよく見かけるアプローチだけでも res.type, res.set, res.header, res.setHeader, res.writeHead がある それぞれの書き方、結果について実際にやってみた 例として、HTTPレスポンス時 の HTTPヘッダー Content-Type が Content-Type: text/plain; charset=utf-8 となるようにコードをかいてみる 先に、全体像 → Express で HTTPヘッダをセットする方法一覧 「"Hello" を "Content-Type: text/plain;" ヘッダつけてレスポンスする」いろんなやり方 メソッド コード例 説明 res#type res.type('text/plain');res.send('Hello'); "content-type"ヘッダー設定専用メソッド res.type('.txt');res.send(`Hello`); ファイル拡張子を指定すると適切なContent-Typeを設定してくれる res#set res.set('content-type', 'text/plain');res.send('Hello'); ヘッダー名,値 で指定する res.set({'content-type': 'text/plain','x-original-header': 'original_value'});res.send('Hello'); res#setで、ヘッダーを複数指定する書き方 res#header res.header('Content-Type', 'text/plain');res.send('Hello'); res#set のエイリアス res.header({'content-type': 'text/plain','x-original-header': 'original_value'});res.send('Hello'); ヘッダーを複数指定する書き方 res#setHeader res.setHeader('content-type', 'text/plain');res.send(`Hello`); Node.js の 'http' モジュールに所属するメソッド res#writeHead res.writeHead(200, { 'Content-Type': 'text/plain' });res.write('Hello');res.end();// 応答プロセスを終了する Node.js の 'http' モジュールに所属するメソッドres.sendとは併用できない 本編 Expressで簡易サーバーを書く 実験用簡易サーバーを動作させて、HTTPヘッダーがどのように出力されるか curl で確認する 実験用簡易サーバー import express from 'express'; export default class HttpServer { constructor() { this.server = null; } async start(options = {}) { const { port } = options; const app = express(); app.get('test', (req, res) => { // ここでいろいろ試す res.set('content-type', 'text/plain'); res.send('Hello'); }); return new Promise((resolve) => { this.server = app.listen(port, () => { console.log(`Server started on port:${port}`); resolve(); }); }); } stop() { this.server.close((() => { console.log(`Server stopped`); })); } } (async () => { const server = new HttpServer(); await server.start({ port: 8080 }); })() ヘッダー確認用curl curl --head http://localhost:8080/test res.type res#type をつかう。 res#type は "text/plain" のように"/"が含まれていたら、Content-Type : text/plainのように、そのまま反映するが、"/" 含まれていなければ、指定した文字列をファイル拡張子とみなして、MIMEタイプが検索され適切な Content-Typeが設定される。 コード app.get('test', (req, res) => { res.type('text/plain'); res.send('Hello'); }); 結果 Content-Type: text/plain; charset=utf-8 テキストファイルの拡張子を指定してみる コード app.get('/test', (req, res) => { res.type('.txt'); res.send(`Hello`); }); 結果 ちゃんと "text/plain" になっている。 Content-Type: text/plain; charset=utf-8 こうしても結果は同じだった app.get('/test', (req, res) => { res.type('txt'); res.send(`Hello`); }); res.set() res#set をつかう コード app.get('/test', (req, res) => { res.set('content-type', 'text/plain'); res.send('Hello'); }); 以下のように書くこともできる。こちらは、複数ヘッダをセットできる app.get('/test', (req, res) => { res.set({'content-type':'text/plain'}); res.send('Hello'); }); 結果 HTTPヘッダーは以下のとおり。charset が自動付与されているが、Expressではデフォルトcharsetは"utf-8"となる。 Content-Type: text/plain; charset=utf-8 実際の応答はこんな感じ。ひとまず Content-Type に着目する。 curl --head http://localhost:8080/test HTTP/1.1 200 OK X-Powered-By: Express Content-Type: text/plain; charset=utf-8 Content-Length: 5 ETag: W/"5-9/+ei3uy4Jtwk1pdeF4MxdnQq/A" Date: Thu, 24 Feb 2022 05:05:55 GMT Connection: keep-alive Keep-Alive: timeout=5 res#setでヘッダを複数指定してみる app.get('/test', (req, res) => { res.set( { 'content-type': 'text/plain', 'x-original-header': 'original_value' }); res.send('Hello'); }); 結果 ちゃんと複数指定できていた。 Content-Type: text/plain; charset=utf-8 x-original-header: original_value res.header res#header は res#set のエイリアス なので、 res#set と等価(おんなじ) コード app.get('/test', (req, res) => { res.header('Content-Type', 'text/plain'); res.send('Hello'); }); 結果 Content-Type: text/plain; charset=utf-8 setメソッドと同じく複数指定も可 app.get('/test', (req, res) => { res.header( { 'content-type': 'text/plain', 'x-original-header': 'original_value' }); res.send('Hello'); }); res.setHeader res#setHeaderは Express ではなく Node.js のコアモジュールである 'http' モジュールがもつメソッド コード app.get('/test', (req, res) => { res.setHeader('content-type', 'text/plain'); res.setHeader('x-original-header', 'original_value'); res.send(`Hello`); }); 結果 Content-Type: text/plain; charset=utf-8 x-original-header: original_value res.writeHead res#writeHead は Express ではなく Node.js のコアモジュールである 'http' モジュールがもつメソッド コード res.writeHead は res.send と併用できないので注意(理由は後述) app.get('/test', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write('Hello'); res.end();// 応答プロセスを終了する }); 結果(ヘッダーぜんぶのせ) curl --head http://localhost:8080/test HTTP/1.1 200 OK X-Powered-By: Express Content-Type: text/plain Date: Thu, 24 Feb 2022 05:50:41 GMT Connection: keep-alive Keep-Alive: timeout=5 res.writeHeadとres.sendを併用できない理由 うっかり以下のようにしてしまうとエラーとなる app.get('/test', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.send(`hello`); }); エラー Cannot set headers after they are sent to the client これは writeHead を実行すると、即座にレスポンスコードとヘッダ書き込みが起こるが、 res.send 内でも、再度 ヘッダ書き込みを行うため。 おまけ res.send 後に res.end は不要 app.get('/test', (req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.write('Hello'); res.end();// 応答プロセスを終了する }); この場合は res.endで応答のプロセスを終了しているが、 res.send の場合は res.send 内で res.end をしているので、 res.end は不要となる。 おまけ、その2: Content-Type ヘッダーを送出しない方法 以下のように、何も返さないで出力を終える ということもできる この場合、 Content-type は付与されない コード app.get('/test', (req, res) => { res.status(200).end(); }); 結果(ヘッダーぜんぶのせ) curl --head http://localhost:8080/test HTTP/1.1 200 OK X-Powered-By: Express Date: Thu, 24 Feb 2022 05:59:52 GMT Connection: keep-alive Keep-Alive: timeout=5 こうやっても同様に何も返さないで出力をおえる コード app.get('/test', (req, res) => { res.status(200).send(); }); 結果(ヘッダーぜんぶのせ) curl --head http://localhost:8080/test HTTP/1.1 200 OK X-Powered-By: Express Date: Thu, 24 Feb 2022 06:03:10 GMT Connection: keep-alive Keep-Alive: timeout=5 res.send で空文字を返すと Content-Type は付与される コード例 app.get('/test', (req, res) => { res.status(200).send(''); }); 結果(ヘッダーぜんぶのせ) Content-Length: 0 だが、Content-Type は text/html が付与される。 >curl --head http://localhost:8080/test HTTP/1.1 200 OK X-Powered-By: Express Content-Type: text/html; charset=utf-8 Content-Length: 0 ETag: W/"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk" Date: Thu, 24 Feb 2022 06:03:34 GMT Connection: keep-alive Keep-Alive: timeout=5 関連
- 投稿日:2022-02-24T14:21:17+09:00
【JavaScript】Array, Object周りのメソッドを活用しよう
はじめに JavaScriptのArrayやObjectには便利なメソッドが備わっています. 適切に使ってコードを読みやすく保ちましょう,というお話です. 個人的によく使うメソッドを紹介していきます. アロー関数式 本記事のコードではアロー関数式を用います.アロー関数式は関数式の代替構文です. 以下の2文は同じ意味になります.(他にも書き方は色々あります.) // 伝統的な関数 let double = function (val) { return val*2 } // アロー関数 let double = (val) => val*2 Arrayのメソッド 1. find 引数として与えたテスト関数を配列の先頭要素から適用し,一番最初に合格した(truthyな値を返した)要素を返します.どれも合格しなければundefinedを返します. let ary = [ {val: 100, name: 'aaa'}, {val: 200, name: 'bbb'}, {val: 300, name: 'ccc'}, ] ary.find((elm) => elm.val == 200) // -> {val: 200, name: 'bbb'} ary.find((elm) => elm.name == 'ddd') // -> undefined 2. findIndex 引数として与えたテスト関数を配列の先頭要素から適用し,一番最初に合格した(truthyな値を返した)要素の位置を返します.どれも合格しなければ-1を返します. let ary = [ {val: 100, name: 'aaa'}, {val: 200, name: 'bbb'}, {val: 300, name: 'ccc'}, ] ary.findIndex((elm) => elm.val == 200) // -> 1 ary.findIndex((elm) => elm.name == 'ddd') // -> -1 3. indexOf 引数として与えた要素を先頭から検索し,一番最初に見つけた位置を返します.見つからなければ-1を返します. let ary = [ {val: 100, name: 'aaa'}, {val: 200, name: 'bbb'}, {val: 300, name: 'ccc'}, ] let target = ary[1] ary.indexOf(target) // -> 1 要素の中身までは見ません.Objectなどの場合は同じインスタンスでなければいけません. [0,1,2].indexOf(2) // -> 2 [{val:0}, {val:1}, {val:2}].indexOf({val:2}) // -> -1 4. includes 引数として与えた要素が配列内に含まれるかどうかをtrueかfalseで返します. let ary = [ {val: 100, name: 'aaa'}, {val: 200, name: 'bbb'}, {val: 300, name: 'ccc'}, ] let target = ary[1] ary.includes(target) // -> true こちらもindexOfと同様,要素の中身までは見ません. [0,1,2].includes(2) // -> true [{val: 0}, {val: 1}, {val: 2}].includes({val: 2}) // -> false 5. filter 引数として与えたテスト関数に合格する要素のみを残し,新しい配列を生成して返します. let ary = [{val: 100}, {val: 150}, {val: 200}, {val: 250}] ary.filter((elm) => elm.val%100 == 0) // -> [{val: 100}, {val: 200}] ary.filter((elm) => elm.val >= 200) // -> [{val: 200}, {val: 250}] 6. map 引数として与えた関数を各要素に適用し,その結果からなる新しい配列を生成して返します. let ary = [ {val: 100, name: 'aaa'}, {val: 200, name: 'bbb'}, {val: 300, name: 'ccc'}, ] ary.map((elm) => { return {val: elm.val/100, name: elm.name[0]} }) // -> [{val: 1, name:'a'}, {val: 2, name:'b'}, {val: 3, name:'c'}] 7. every 配列の全要素が,引数として与えたテスト関数に合格するかどうかをtrueかfalseで返します. let ary = [ {val: 100, name: 'aaa'}, {val: 200, name: 'bbb'}, {val: 300, name: 'ccc'}, ] ary.every((elm) => elm.val%100 == 0) // -> true ary.every((elm) => elm.val == 200) // -> false 8. some 配列内の少なくとも1つの要素が,引数として与えたテスト関数に合格するかどうかをtrueかfalseで返します. let ary = [ {val: 100, name: 'aaa'}, {val: 200, name: 'bbb'}, {val: 300, name: 'ccc'}, ] ary.some((elm) => elm.val%100 == 0) // -> true ary.some((elm) => elm.val == 200) // -> true Objectのメソッド 9. assign 1つ以上のコピー元オブジェクトから列挙可能なプロパティを,コピー先オブジェクトにコピー(シャローコピー)します. let obj1 = {val: 100, name: 'aaa'} let obj2 = {val: 200, age: 20} Object.assign(obj1, obj2) // obj1 -> {val: 200, name: 'aaa', age: 20} // obj2 -> {val: 200, age: 20} 複数のオブジェクトを結合した「新しい」オブジェクトを作成したい場合は以下のようにも書けます. let new_obj = {...obj1, ...obj2} // new_obj -> {val: 200, name: 'aaa', age: 20} // obj1 -> {val: 100, name: 'aaa'} // obj2 -> {val: 200, age: 20} 10. fromEntries キーと値の組み合わせのリストから,オブジェクトに変換します. (個人的にこのメソッドは分かりづらいので,for文で書いても良いと思います.) let items = [['val', 100], ['name', 'aaa'], ['age', 20]] Object.fromEntries(items) // -> {val: 100, name: 'aaa', age: 20} for文で書くなら以下. let items = [['val', 100], ['name', 'aaa'], ['age', 20]] let obj = {} for (let [k,v] of items) { obj[k] = v; } // obj -> {val: 100, name: 'aaa', age: 20} なぜ用意されているメソッドを使うべきか? 本記事で紹介したメソッドの基本動作は簡単で,少しプログラミングを勉強した方であればfor文などを使って自力で書けると思います. であればなぜJSに用意されているメソッドをわざわざ覚えて使うのか? 私は以下の理由を挙げます. バグを生んでしまう可能性が減る 何の処理なのか一目で分かる 行数が少なくなり,プログラムの見通しが良くなる いくら経験を積んでも,間違い(=バグ)は起きます.少しでもバグの発生要因になることは減らしましょう. また,未来の自分や他人がそのプログラムを見るときのために,わかりやすさは重要です. JavaScriptの動作は比較的早いですし,ブラウザであれば処理するデータもそこまで大きくないことが多いです. そのため個人的には,少しぐらい処理が冗長になったとしても,用意されているメソッドを使うべきだと思っています. 参考 紹介した以外にもいくつかのメソッドが用意されています. 詳しい仕様などはMDN Web Docsが参考になると思います.
- 投稿日:2022-02-24T12:09:31+09:00
可変長の連想配列の型指定方法について
連想配列の型指定について自分のメモ用を兼ねて投稿します。 const Fruits = { fruit1: "apple", fruit2: "orange", fruit3: "banana", ... fruit10: "grape" } 上記のような連想配列があるとします。 オブジェクトの長さは10固定ではなく、長さが分からない設定です。 自分は最初下記のような型をどうやって一般化しようか考えていました。 しかし、そんなに難しく考えなくても良かったです。 type FruitsType = { fruit1: "string"; fruit2: "string"; fruit3: "string"; ... fruit10: "string"; } 下記のようにすれば瞬殺です。 type FruitsType = { [key: string]: string } 連想配列のkeyに対して型指定ができるのは知らなかったです・・・ keyはstring型とnumber型を指定できるようです。 参考URL https://golang.hateblo.jp/entry/2021/03/15/202502
- 投稿日:2022-02-24T12:02:54+09:00
Stripe Billingで、サブスクリプションのプラン・料金変更後の請求金額を事前にプレビューする
Stripe Billingでサブスクリプションを提供している場合、契約期間中のプラン変更で「使った分だけ支払う」決済ができます。Stripeではこれを「比例配分」とよんでいます。 例えば「月1,000円のプラン」を途中で「月2,000円のプラン」に変更する場合、比例配分が有効になっていると、以下のような請求書が作られます。 1: 月1,000円のプランの未使用分: ▲ 743円 2: 月2,000円のプランの使用分: 1,485円 3: 次回の契約期間の利用料金: 2,000円 この請求のうち、1と2が比例配分によるものです。 1は、「変更前プランの、プラン変更日から契約期間終了日までの日割料金」を請求から差し引いています。 そして2では、、「変更後プランの、プラン変更日から契約期間終了日までの日割料金」を新しく請求します。 3はこの次の契約期間の利用料金で、この3つを次の契約期間の請求書で決済することになります。(1と2だけ即時決済することも可能) これにより、「プラン変更日前までは月1,000円のプラン、変更日からは月2,000円のプラン」の料金のみを顧客が支払ったことにできます。 比例配分を有効化すると、次回の請求金額の見積もりが難しくなる この比例配分があることで、顧客は「使った分だけ払えば良い」ことになり、プランのアップグレードへの懸念を減らすことができます。 ただし、「プランをいつ更新するか」によって次回の請求金額が変わるため、プラン変更画面で「次回の請求金額」を顧客に提示することが難しくなる問題が発生します。 また、Stripeでは日割り計算を秒単位で行うため、自前で金額計算の実装は難易度が高くなります。 InvoicesのRetrieve Upcoming APIを利用する この比例配分された金額を計算するために利用できるのが、InvoicesのRetrieve Upcoming APIです。 このAPIを利用することで、「サブスクリプションの次回請求予定金額」を取得することができます。 const upcoming = await stripe.invoices.retrieveUpcoming({ customer: 'cus_xxx', subscription: 'sub_xxxx', }) このコードでは、指定した顧客・サブスクリプションの次回請求予定金額を取得します。 もし、プラン変更後の金額をプレビューしたい場合は、以下のように変更後のitemsを設定しましょう。 const customerId = 'cus_xxx' const subscriptionId = 'sub_xxxx' + const subscription = await stripe.subscriptions.retrieve(subscriptionId) + const subscriptionItems = [...subscription.items.data.map(item => ({ + id: item.id, + deleted: true, + })),{ + price: 'price_xxxx', + quantity: 1 + }] const upcoming = await stripe.invoices.retrieveUpcoming({ customer: customerId, subscription: subscriptionId, + subscription_items: subscriptionItems, }) この際に注意が必要なのは、「変更前の料金データを、明示的に削除する」必要があることです。 上のコードでは、先に現在のサブスクリプションデータを取得し、契約中のプランを全て削除する操作が実装されています。 const subscription = await stripe.subscriptions.retrieve(subscriptionId) const subscriptionItems = [...subscription.items.data.map(item => ({ id: item.id, deleted: true, })),{ price: 'price_xxxx', quantity: 1 }] もし削除せず、新しい料金データの設定だけを行うと、「追加でこの料金も契約する」と判断されますのでご注意ください。 プレビュー金額と一致させるため、サブスクリプションの更新タイミングを明示的に指定する APIによって「いまプランを変更した場合、次回の請求がどうなるか」をプレビューできるようになりました。しかしまだ問題は残っています。 それは、「比例配分は秒単位で実施されるため、プレビューの金額と実際の請求額が完全に一致しない」問題です。 プレビューと更新を同じタイミングで実施することは不可能です。 そのため、「この時間に更新した場合、次回の請求はこの内容です」のように、「更新タイミングを指定」する必要があります。 Stripeで更新タイミングを指定するには、proration_dateを利用します。 以下のサンプルコードでは、Day.jsを利用し、「その日の終わりにプラン変更を実行する」設定を行なっています。 const customerId = 'cus_xxx' const subscriptionId = 'sub_xxxx' + const prorationDate = dayjs().endOf('date') const subscription = await stripe.subscriptions.retrieve(subscriptionId) const subscriptionItems = [...subscription.items.data.map(item => ({ id: item.id, deleted: true, })),{ price: 'price_xxxx', quantity: 1 }] // 請求書のプレビューを取得する場合 const upcoming = await stripe.invoices.retrieveUpcoming({ customer: customerId, subscription: subscriptionId, subscription_items: subscriptionItems, - subscription_proration_date: prorationDate + subscription_proration_date: prorationDate.unix() }) // サブスクリプションを実際に更新する場合 await stripe.subscriptions.update(subscriptionId, { + proration_date: prorationDate.unix(), items: subscriptionItems, }) プレビューの明細を確認する APIで取得したデータには、合計金額だけでなく明細も含まれています。 明細の表示には、upcoming.lines.dataのデータを利用します。 const upcoming = await stripe.invoices.retrieveUpcoming({ customer: customerId, subscription: subscriptionId, subscription_items: subscriptionItems, subscription_proration_date: prorationDate.unix() }) let amount = 0 upcoming.lines.data.forEach((line, i ) => { console.log(`[${i}]${line.description} - ${line.amount}`) amount = amount + line.amount }) console.log({ amount, upcomingAmount: upcoming.amount_due, }) このコードを実行すると、明細と次回請求金額のプレビューが表示されます。 console.log [0]Unused time on Starter plan after 15 Feb 2022 - -814 console.log [1]Remaining time on Business plan after 15 Feb 2022 - 1221 console.log [2]1 × Business plan (at ¥1,500 / month) - 1500 console.log { amount: 1907, upcomingAmount: 1907 } 明細には以下の3種類が含まれています。 Unused: 変更前のプランの未使用分(変更予定日から本来の更新予定日までの日割り分) Remaining: 変更後のプランの使用分(変更予定日から本来の更新予定日までの日割り分) 変更後のプラン: 次回のサイクルの請求金額 after 15 Feb 2022部分が、更新予定日の日付です。 現時点では、Stripe側では明細内容は完全に翻訳されません。 そのため、顧客からの問い合わせが想定される場合には、あらかじめヘルプドキュメントなどを作成しましょう。 またここで表示されている金額を全て足し合わせると、upcoming.amount_dueの金額と一致します。 もし0円を下回る場合は、0円として処理され、マイナスの金額はその次以降の請求内容と相殺されます。 契約期間(インターバル)が異なる料金への変更 月額から年額への変更などでも、「subscription.update APIが実行可能であれば」プレビューを取得できます。 1つのサブスクリプションには、1つの契約期間しか設定できません。 そのため、契約期間の異なる料金へ変更する場合は、必ずそれまで利用していた料金全てを削除する必要があります。 const subscription = await stripe.subscriptions.retrieve(subscriptionId) const subscriptionItems = [...subscription.items.data.map(item => ({ id: item.id, deleted: true, })),{ price: 'price_xxxx', quantity: 1 }] const upcoming = await stripe.invoices.retrieveUpcoming({ customer: customerId, subscription: subscriptionId, subscription_items: subscriptionItems, }) 「複数の契約期間が混在する状態」ですと、以下のようにエラーが発生しますので、ご注意ください。 StripeInvalidRequestError: Currency and interval fields must match across all plans on this subscription. Found mismatch in interval field. 従量課金プランへの変更について 従量課金プランへの変更についても、「更新可能な設定」であればプレビューができます。 ただし、従量課金プランの場合、使用量計測前の金額でプレビューされるため、あまり意味を成さないことにご注意ください。 段階的・数量ベースで、定額課金も含むプランの場合 基本的には、どのプランでもプレビューは可能です。 ただし、設定によっては、請求書明細の行数が長くなる場合がありますので、デザインやCSSの設定にはご注意ください。 関連ドキュメント [PR] Stripe開発者向け情報をQiitaにて配信中! [Stripe Updates]:開発者向けStripeアップデート紹介・解説 ユースケース別のStripe製品や実装サンプルの紹介 Stripeと外部サービス・OSSとの連携方法やTipsの紹介 初心者向けのチュートリアル(予定) など、Stripeを利用してオンラインビジネスを始める方法について週に2〜3本ペースで更新中です。 -> Stripe Organizationsをフォローして最新情報をQiitaで受け取る
- 投稿日:2022-02-24T08:48:00+09:00
No 'Access-Control-Allow-Origin' header is present on the requested resource. を無理やり克服する
概要 Access to image at 'https://{URL} from origin 'https://{ドメイン}' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. S3 に画像や動画を展開した際に、稀によく出るこのメッセージ。 ブラウザ上で表示は出来ているのに、javascriptで画像などを利用しようとするとエラーとなり、苦い思いもしたので備忘録として対応を残しておきます。 症状 上記のエラーが出たり出なかったり。 ブラウザを変えたり、シークレットモードで表示するとエラーが出なくなったりもする。 原因 このエラーメッセージで検索すると、S3のCORS設定 や CloudFrontのヘッダー設定 をする 記事が多いですが、それらの設定が正常に終了しているという前提でいうと、原因はブラウザのキャッシュです。 ブラウザにキャッシュされた画像を利用した際にエラーになります。 対策 詳しい原因はさておき、対策方法。 やっていることは単純で、パラメータを変えて三回読み直しています。 image_load.js function imageLoad(image_url) { let try_count = 3; let img = new Image(); let original_url = image_url.split('?')[0]; img.onload = () => { /* 画像の読み込みが終わった後にやりたいこと */ }; img.onerror = () => { if (try_count > 0) { img.src = original_url + '?' + Math.random().toString(32).substring(2); try_count--; } else { /* 画像の読み込めなかった後のこと */ } }; img.crossOrigin = 'Anonymous'; img.src = image_url; } 取り急ぎご報告まで。
- 投稿日:2022-02-24T02:13:44+09:00
辞書(dict)型データを、Spreadsheetに取り込む。SmartHRのデータを想定
この記事は何? SmartHRデータ加工入門として作った記事の第3弾であり、最後の記事です。 https://qiita.com/piyoSakai/items/2982fb7511331e56f8fb https://qiita.com/piyoSakai/items/21efd055635b92d14a7a サンプルコード こちらの記事で加工したデータを、Spreadsheetに突っ込む。 const LOADING_SHEET_NAME = 'Crews'; //ここは各自のシート名に合わせる function crews_to_list(crews){ let col = [] for(let c of crews){ let row = [ c['email'], c['last_name'], c['first_name'], c['position'], c['is_corporate'], c['is_hr'], c['is_ms'], c['is_cp'], ]; col.push(row); } return col; } function clear(sheet){ // ざっくりクリアする。なのでLOADING_SHEETにはメモ等残せない。 sheet.getRange(2, 1, 999, 100); } function load(sheet, data){ sheet.getRange(2, 1, data.length, data[0].length).setValues(data); //ヘッダ行を残す }; function load_smarthr_data(){ let crews = extract('crews'); //1つ目の記事の処理 let summarized_crews = translate(crews); //2つ目の記事の処理 let data_to_load = crews_to_list(summarized_crews); const spreadsheet = SpreadsheetApp.openById(SHEET_ID); let sheet = spreadsheet.getSheetByName(LOADING_SHEET_NAME); clear(sheet); load(sheet, data_to_load); } 解説 load_smarthr_data()で、これまでのデータ抽出(Extract)、加工(Translate)を行っている その後、辞書(dict)データを、Spreadsheetに追加しやすい配列(list)に変換ている 最後に、シートをクリアし、データを突っ込んでいる もう少しエレガントに書ける気もするが、一旦このくらいがわかりやすいかもしれない。 ちなみに、著者は最初これらをPythonで書いていたのだが、 定期実行が容易であること GoogleSpreadsheetへの吐き出しが容易であること から、GASにわざわざ移植した。 一方で、型アノテーションができないなど一長一短はあるので、身近な環境から使う言語を選定するのがおすすめ。 発展編 このデータを活用して、Googleのユーザーグループを編集することができる。 このようにGoogleSpreadsheetを中間データベース兼UIとして活用することで、非エンジニアでも視覚的に理解しやすいUIでコーポレートエンジニアリングを行うことが出来る。 https://qiita.com/piyoSakai/items/e0e517c090ab061936a2
- 投稿日:2022-02-24T02:02:59+09:00
SmartHRのデータを上手に加工する
サンプルコード こちらの記事で取得したデータを、扱いやすいように加工する。 特に部署情報について、階層構造をフラットにし、情報をタテ持ちする。 function get_top_depertment(depertment, prev_depertment = null){ if(depertment !== null){ result = get_top_depertment(depertment['parent'], depertment); return result; }else{ return prev_depertment; } } function get_middle_depertment(depertment, prev_depertment = null, prev_prev_depertment = null){ if(depertment !== null){ result = get_middle_depertment(depertment['parent'], depertment, prev_depertment); return result; }else{ return prev_prev_depertment; } } // 上層から1,2番目の部署と、与えられた部署名を比較して、一致する部署があればTrueを返す function is_member_of_x(departments, department_name){ if(departments !== null){ for(let dept of departments){ top_depertment = get_top_depertment(dept); if(top_depertment !== null && top_depertment['name'] == department_name){ return true; }else{;} middle_depertment = get_middle_depertment(dept) if(middle_depertment !== null && middle_depertment['name'] == department_name){ return true; }else{;} } return false; }else{ return false; } } function is_none_or_empty(value){ if(value === null || value == ''){ return true; }else{ return false; } } function translate(data){ let result = [] for(let item of data){ let business_last_name = (('business_last_name' in item) ? item['business_last_name'] : null); let business_first_name = ('business_first_name' in item ? item['business_first_name'] : null); let last_name = ('last_name' in item ? item['last_name'] : null); let first_name = ('first_name' in item ? item['first_name'] : null); let departments = ('departments' in item ? item['departments'] : null); let position = ('position' in item ? item['position'] : null); let email = (item['custom_fields'].length > 0 ? item['custom_fields'][0]['value'] : null); result = result.concat({ 'last_name': (is_none_or_empty(business_last_name) ? last_name : business_last_name), 'first_name': (is_none_or_empty(business_first_name) ? first_name : business_first_name), 'email': email, 'position': position, 'is_corporate': is_member_of_x(departments, 'コーポレート本部'), 'is_hr': is_member_of_x(departments, '人事部'), //Human Resources 'is_ms': is_member_of_x(departments, '管理基盤部'),//Management System 'is_cp': is_member_of_x(departments, '経営企画部'),//Corporate Planning }) } return result } 解説 is_member_of_x()が読解難易度高いかもしれない。 これは、X部署に所属しているかどうかを、部署情報を渡して判別するメソッド。 (ただし、簡単のため上層から1~2階層に判定をとどめている、拡張も可能ではある) SmartHRの部署情報は以下のように再帰構造になっている。 部署[,,,parent:部署[,,,parent:部署[,,,parent:部署]]] そこで、get_top_depertmentでは再帰を使って最上位部署を特定する手法を取っている。 初学者は、「再帰」についてググると、似た構造を見つけられるかもしれない。 きちんと理解したほうが良いが、いったんおまじないとして理解を後回しにしてもそこまで困らない。 次回はこのデータのSpreadsheetへのロードについて解説する。
- 投稿日:2022-02-24T01:53:46+09:00
SmartHRのデータをGASで引っ張ってくるサンプル
前提 公式ドキュメントを見るのが一番いいですが、初心者向けに。 サンプルコード 従業員全員の情報を引っ張ってくるサンプル const TENANT_ID = 'YOUR_COMPANIYS_TENANT_ID'; const ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN'; const STAY_SECOND = 1; function is_last_page(headers){ try{ links = headers['link']; //nextのURL表記が最終ページで無くなる if('next' in links){ return false; }else{ return true; } }catch(e){ if(e instanceof TypeError){ // 1ページで完結する場合はそもそもlink要素が無い return true; }else{ throw e; } } } function object_to_param(obj){ return Object.keys(obj).map(function(key){ return key+"="+ obj[key] }).join("&") } function extract(){ let HEADERS = { 'Authorization': 'Bearer '+ ACCESS_TOKEN }; result = [] // 安全のため上限を100回に設定 for(let i = 1; i < 100; i++){ let url = 'https://' + TENANT_ID + '.smarthr.jp/api/v1/crews?'; let payload = { 'page': i, 'per_page': 100, 'emp_status': 'employed', //ここらへんはご自身のやりたいことに合せて追加・削除してください。 } let options = { 'method' : 'GET', 'headers' : HEADERS, } let response = UrlFetchApp.fetch(url + object_to_param(payload), options); let result = result.concat(JSON.parse(response)); if(is_last_page(response.getHeaders())){ break; }else{ Utilities.sleep(STAY_SECOND * 1000); } } return result; } function main(){ Logger.log(extract()); } 解説 APIを叩いているextract()では、ページ遷移が発生するためループ構造になっている。 is_last_page()でページ遷移における最終ページかの判定をしている。もっとエレガントな方法もあるかも。 object_to_param()は、オブジェクトをGETメソッドのパラメタ文字列に変換している。GASのGETではoptionsにpayloadとして追加することはできないっぽい。 次回はその後の加工について記載する。
- 投稿日:2022-02-24T00:58:31+09:00
セレクトボックスで選択したテキスト情報を出力する
このようなセレクトボックスとテキストボックスがあったとする セレクトボックスの中身はこう htmlソースは以下 hanninsagashi.html <div>犯人 <select name='hannin' id='hannin'> <option value="1">本人</option> <option value="2">夫</option> <option value="3">娘</option> <option value="4">その他家族</option> <option value="5">該当なし</option> </select> </div> <div> <td> <textarea name="memo" id="memo" style="height: 50px; width: 150px"></textarea> </td> </div> セレクトボックスで選択した結果のテキスト情報をtextareaに出力させたい場合、 セレクトボックスで選択したvalueのindex値をselectedIndexを使用して取得した上で textContentで出力するという方法でいける purintabetadaro.js const shinhannin = () => { const hannin = document.getElementById("hannin"); const memo = document.getElementById("memo"); const hanninIndex = hannin.selectedIndex; memo.value = hannin.options[hanninIndex].textContent; } document.getElementById("hannin").onchange = shinhannin; このようにセレクトボックスのプルダウンのテキストがそのまま出てくれる セレクトボックスをJavascriptのオブジェクト的に表現した場合、以下になるということを 念頭に置いたらわかりやすいかもしれない {value : 1 , name : "本人"}, //index : 0 {value : 2 , name : "夫"}, //index : 1 {value : 3 , name : "娘"}, //index : 2 {value : 4 , name : "その他家族"}, //index : 3 {value : 5 , name : "該当なし"} //index :4 つまり、夫(index値:1)を選択した場合、「document.getElementById("hannin").selectedIndex = 1」 ということになる よって、「document.getElementById("hannin").options[1].textContent = 夫」 という結果になる なお、「Cannot set property 'onchange' of null」が出た場合、 Javascriptの記述をHtmlの後に記述するなどの対処が必要