https://nexpr.gitlab.io/public-pages/promise-merge/
Promise.all とか Promise.any とか Promise をまとめる関数があります
よく使う all はいいのですが たまに使うものだとどれだっけ?ってなることがあります
また誰かに伝えるときに説明ってしづらかったりするのですよね
ボタンを押して resolve/reject して結果が見れるのがあるとわかりやすいかなと思って確認できる画面を作ってみました
使い方は見たままですが……
一番上のボタンで all/any/race/allSettled を選びます
固定で 3 つの Promise が作られて それぞれの resolve/reject ボタンを押せます
押すとその Promise の状態が更新されます
3 つの Promise を指定の方法 (all や race) でまとめた結果の Promise の状態が一番下に出ます
const css = new CSSStyleSheet()
css.replaceSync(text)
これで作った CSSStyleSheet オブジェクトから CSS のテキストを取り出したいです
目的は text の変数に入ってる CSS を有効な形に修正することです
HTML や CSS は構文がおかしくてもブラウザ側で不正なものは適当に修正してくれるのでその結果がほしいです
HTML なら
const div = document.createElement("div")
div.innerHTML = `<div><p>aa`
div.innerHTML
// '<div><p>aa</p></div>'
というふうにできるのですが CSS ってこういうのがないです
cssRules にルールが入ってますが 色々分かれてしまっています
いい感じに取り出すのは難しそうかなと思ってたのですが 単純に個々の cssRules の cssText を取得して結合すればいい感じになりました
「@」 を使ったものやネストなどに不安がありましたが MDN にあるサンプルコードを適当にコピペして試した感じではほぼ完全に復元できました
以下が確認済みです
@font-face
@keyframes
@media
@page
@container
@layer
@namespace
@counter-style
@supports
@property
@scope
@import は replace / replaceSync で使えないので確認してません
@charset は出力されなかったですがパース後には元の文字コードを保持する必要がないのでそういうものなのだと思います
もしかするとマイナーなところで出力できない部分があるかもしれないですが 基本的なものでは問題なく使えるレベルだと思います
const formatCSS = (text) => {
const css = new CSSStyleSheet()
css.replaceSync(text)
return [...css.cssRules].map(rule => rule.cssText).join("\n")
}
少し気になったところでは ネストするとインデントがおかしくなります
console.log(formatCSS(`
.foo {
.bar {
.baz {
}
}
}
`))
.foo {
.bar {
.baz { }
}
}
読むことを目的としてないので実害はないですが ネストの対応はこの辺まで完璧じゃないようですね
ちなみにこの方法 自分で作った CSSStyleSheet 以外の style タグや link タグでも使えました
link タグの場合は取得する CSS ファイルが同じオリジンである必要があります
const sheetToCSS = (sheet) => {
return [...sheet.cssRules].map(rule => rule.cssText).join("\n")
}
console.log(sheetToCSS(document.querySelector("style").sheet))
console.log(sheetToCSS(document.querySelector(`link[rel="stylesheet"]`).sheet))
Solid.js の Playground では ページ内に Chrome の DevTools が埋め込まれています(右下)
https://playground.solidjs.com/
どうなってるのと思ってソースコードを見てました
https://github.com/solidjs/solid-playground
chii というプロジェクトで DevTools の画面を作ってるようです
ウェブフロントエンドで動くようにパッチを当てていますが DevTools のソースコードが使われてます
また DevTools と接続するページ側では chobitsu というライブラリを使って DevTools との通信を管理してるようです
chobitsu は CDP の JavaScript 実装らしく CDP ライブラリなら他にも類似のものは色々ありそうですが chii との通信専用に独自の部分があるのかもです
Vue にしてもそうですが中国のライブラリって日本アニメの名前が使われる傾向があるみたいですね
Solid.js の Playground のページは Solid.js が前提になっていたり外部から更新できる画面になっていて複雑だったのでシンプルに最低限の機能で動くページを作ってみました
Solid.js の Playground のページから多くコードを流用してます
https://nexpr.gitlab.io/public-pages/chrome-devtools/
仕組みとしてはメインのページがあってその中に 2 つの iframe があります
片方が DevTools の画面で もう片方が DevTools と接続する画面です
それぞれの画面が親フレームにメッセージを送るようになっているので メインのページではメッセージを相手側に送信するようします
Elements タブで要素にマウスを乗せると対応する部分に色がついたり要素サイズが表示されたり ちゃんと DevTools の動きをしています
Console タブで JavaScript コードを実行できますし DOM を更新すればそれに対応して画面も変わります
ほとんどいつもの DevTools と変わらないですが 制限も色々あります
コンソールから Sources タブに飛んでエラー箇所を確認できなかったり
デバッグ実行ができなかったり
Elements タブで一部のスタイルが適用されているのに確認できなかったり(外部 CSS がだめなのかも)
すごいライブラリではあるのですが 制限があることで中途半端になりますし普通に DevTools を出せばいいかなと思うので今後使うかというと使わないかもです
https://playground.solidjs.com/
どうなってるのと思ってソースコードを見てました
https://github.com/solidjs/solid-playground
chii というプロジェクトで DevTools の画面を作ってるようです
ウェブフロントエンドで動くようにパッチを当てていますが DevTools のソースコードが使われてます
また DevTools と接続するページ側では chobitsu というライブラリを使って DevTools との通信を管理してるようです
chobitsu は CDP の JavaScript 実装らしく CDP ライブラリなら他にも類似のものは色々ありそうですが chii との通信専用に独自の部分があるのかもです
Vue にしてもそうですが中国のライブラリって日本アニメの名前が使われる傾向があるみたいですね
Solid.js の Playground のページは Solid.js が前提になっていたり外部から更新できる画面になっていて複雑だったのでシンプルに最低限の機能で動くページを作ってみました
Solid.js の Playground のページから多くコードを流用してます
https://nexpr.gitlab.io/public-pages/chrome-devtools/
仕組みとしてはメインのページがあってその中に 2 つの iframe があります
片方が DevTools の画面で もう片方が DevTools と接続する画面です
それぞれの画面が親フレームにメッセージを送るようになっているので メインのページではメッセージを相手側に送信するようします
Elements タブで要素にマウスを乗せると対応する部分に色がついたり要素サイズが表示されたり ちゃんと DevTools の動きをしています
Console タブで JavaScript コードを実行できますし DOM を更新すればそれに対応して画面も変わります
ほとんどいつもの DevTools と変わらないですが 制限も色々あります
コンソールから Sources タブに飛んでエラー箇所を確認できなかったり
デバッグ実行ができなかったり
Elements タブで一部のスタイルが適用されているのに確認できなかったり(外部 CSS がだめなのかも)
すごいライブラリではあるのですが 制限があることで中途半端になりますし普通に DevTools を出せばいいかなと思うので今後使うかというと使わないかもです
なにかを試すときに一時的なコンテナを作ってそこで作業してることがよくあります
そこで作ったものを Windows 側で使いたいなんてことがときどきあります
Windows 側とまで行かなくてもホスト側の WSL に持ってきたいこともあります
また逆に Windows 側で用意したファイルをコンテナで使いたかったりします
それを見越して とりあえずで docker run するときにカレントディレクトリをコンテナの /mnt にマウントするようにしてるのですが 時々忘れます
そして忘れたときに限ってコピーしたくなったりするものです
コンテナを作り直してもいいのですが 色々パッケージをインストールしたり 環境の設定を変えたりしているともう一度やり直しは面倒です
その時点のコンテナをイメージ化してそこからコンテナを作るという方法もとれますが 一時的なもののためにイメージ化するのも面倒です
それならコンテナを作り直しでもいいかなと思うくらい
ただやっぱり面倒なのでいい方法がないかなと考えていてふと思いつきました
WSL で sshd サーバーを起動しよう
コンテナから WSL には通信できるので scp でコピーできます
やってみると簡単にできました
という感じです
sshd が入ってなければ sshd のインストールとサービスの起動をします
あとはコンテナから
みたいな感じに使います
user は WSL のユーザー名で 172.21.76.78 は WSL の IP アドレスです
WSL まで持ってきたら Windows からは \\wsl$\... のフォルダでアクセスできるので扱いやすいです
以前使ってた方法
◯ http
ウェブサーバーを起動して GET/POST で通信してました
ウェブサーバーは扱いやすいですし GET だけなら python3 がデフォルトで入ってるので http.server モジュールの起動だけで使えるなど楽でした
しかし POST で保存するとなると面倒ですし GET に揃えると送りたい側でサーバーを起動しないといけないです
また フォルダをコピーしたいときには zip 化するなど一手間が必要でした
コンテナ環境だと zip の操作コマンドも入ってなかったりしますし
◯ cifs mount
フォルダのコピーなら共有フォルダがあると便利です
ただ Linux の cifs マウントって結構面倒ですし 頻繁にセットアップ時にうまく行かなくてググってます
オプションも覚えづらくて毎回ググる必要があります
Windows と Docker コンテナ間が直接通信できたらこれでもいいのですが ネットワークが違うので Windows ←→ WSL と WSL ←→ コンテナでしか通信できないです
それなら ssh を通して scp 等のコピーのほうが手軽だと思います
書いた後で思い出しましたが そういえば docker コマンド内にも cp みたいなのがあった気がします
コンテナ内からのコマンドで実行できませんが もうひとつ WSL ウィンドウを開いてホスト側から操作する場合はこっちでもいいかもしれません
そこで作ったものを Windows 側で使いたいなんてことがときどきあります
Windows 側とまで行かなくてもホスト側の WSL に持ってきたいこともあります
また逆に Windows 側で用意したファイルをコンテナで使いたかったりします
それを見越して とりあえずで docker run するときにカレントディレクトリをコンテナの /mnt にマウントするようにしてるのですが 時々忘れます
そして忘れたときに限ってコピーしたくなったりするものです
コンテナを作り直してもいいのですが 色々パッケージをインストールしたり 環境の設定を変えたりしているともう一度やり直しは面倒です
その時点のコンテナをイメージ化してそこからコンテナを作るという方法もとれますが 一時的なもののためにイメージ化するのも面倒です
それならコンテナを作り直しでもいいかなと思うくらい
ただやっぱり面倒なのでいい方法がないかなと考えていてふと思いつきました
WSL で sshd サーバーを起動しよう
コンテナから WSL には通信できるので scp でコピーできます
やってみると簡単にできました
sudo apt install openssh-server
sudo service ssh start
という感じです
sshd が入ってなければ sshd のインストールとサービスの起動をします
あとはコンテナから
scp file.txt user@172.21.76.78:
みたいな感じに使います
user は WSL のユーザー名で 172.21.76.78 は WSL の IP アドレスです
WSL まで持ってきたら Windows からは \\wsl$\... のフォルダでアクセスできるので扱いやすいです
以前使ってた方法
◯ http
ウェブサーバーを起動して GET/POST で通信してました
ウェブサーバーは扱いやすいですし GET だけなら python3 がデフォルトで入ってるので http.server モジュールの起動だけで使えるなど楽でした
しかし POST で保存するとなると面倒ですし GET に揃えると送りたい側でサーバーを起動しないといけないです
また フォルダをコピーしたいときには zip 化するなど一手間が必要でした
コンテナ環境だと zip の操作コマンドも入ってなかったりしますし
◯ cifs mount
フォルダのコピーなら共有フォルダがあると便利です
ただ Linux の cifs マウントって結構面倒ですし 頻繁にセットアップ時にうまく行かなくてググってます
オプションも覚えづらくて毎回ググる必要があります
Windows と Docker コンテナ間が直接通信できたらこれでもいいのですが ネットワークが違うので Windows ←→ WSL と WSL ←→ コンテナでしか通信できないです
それなら ssh を通して scp 等のコピーのほうが手軽だと思います
書いた後で思い出しましたが そういえば docker コマンド内にも cp みたいなのがあった気がします
コンテナ内からのコマンドで実行できませんが もうひとつ WSL ウィンドウを開いてホスト側から操作する場合はこっちでもいいかもしれません
関連
🔗 ESM のみのパッケージが不便
🔗 CJS を ESM に置き換えるのは難しい場合もあった
昔ながらのプロジェクトは相変わらず CJS ですが そろそろ ESM にしようと思って一部書き換えてます
CJS だと ESM のみのパッケージを使うときに 動的 import にするしかなくて非同期処理にせざるを得ないです
自分で Rollup で個別に変換してたときもありましたが 依存パッケージに ESM のみが増えていくとやってられないですし
要望が多くて CJS から同期処理でインポートする手段が提供されるかと思ったりもしてましたが 結局そういうのは入らなそうですし
ESM にしても関連のところに書いたような問題は出てくるのですが CJS から ESM パッケージをインポートするのよりはマシかなというところです
ブロックスコープ内でのエクスポート問題ですが これはひとつのモジュールに色々まとめ過ぎということもあったので ブロックごとに別モジュールにします
モジュールが少ないものだと 5 個が 10 個になるのは倍なので抵抗があったりもしましたが 全体が大きくなって数百もあれば 10 や 20 増えてもたいして気になりませんし 数行しかなくても別モジュールにわけて index.js 的な部分でまとめるようにします
ブロックが if 文なのは条件によってはエクスポートが undefined ということなので宣言的になるよう export const で条件演算子で分岐する形にします
動的な require が import になることで非同期になる問題があります
config ファイルを env の名前を使って読み込むときなどは名前が静的でないので動的 import にせざるを得ません
ですが今はトップレベル await があるので 実質同期的なように初期化できます
読み込み順が変わるので場合によっては問題になることもありますが ほとんどの場合は無視できます
詳しく書くとこういうケースです
結果はこうなります
これが require だと同期処理なので a.js を最初に読み込み a2.js も同期的に読み込まれます
そのあとに b.js が読み込まれるので順番は A → A2 → B → I です
import だと a.js と b.js は並列して取得してから順番に a と b を実行しますが a.js で非同期処理が入ると b.js に進みます
index.js のインポート部分が全部終わらないと index.js の本体の処理は始まりませんが index.js がインポートするモジュールは同時に処理されます
モジュール内で完結してるなら問題ないのですが トップレベルでグローバルに影響する処理をしたり 別モジュールの関数呼び出したりしていると 実行順で期待通りに動かないこともあります
トップレベルではできるかぎり関数等を定義するだけにして処理は行わないようにして 初期化処理が必要なら使う側で init みたいな関数を呼び出してもらい実行するようしたほうがいいかもですね
残る問題はたまにしか使わない機能なので動的に import する場合です
動的インポートなのでその関数が非同期になってしまいます
完全には避けられないので ロードする処理とモジュールを使う処理を分けて 後者の処理は同期処理に保つくらいしかできないです
ただ いつ最初に使うかわからないので 結局チェックしてロードする処理を挟む可能性が常にあって あまり意味がないです
その機能を呼び出す前の段階でその機能を有効にするようなフェーズがあるのなら そこでインポートしておくという使い方はできそうです
あとは 少し面倒な点で CJS のみ対応のライブラリのインポート時にプロパティを直接 named export とみなせません
これができません
という一手間が必要です
Node.js の組み込みモジュールはソースコード上は CJS なのにプロパティを直接参照できるのでなにか方法があるのかと思ったのですが なさそうでした
組み込みモジュールだからこそ特別な対応がされているのでしょうか
🔗 ESM のみのパッケージが不便
🔗 CJS を ESM に置き換えるのは難しい場合もあった
昔ながらのプロジェクトは相変わらず CJS ですが そろそろ ESM にしようと思って一部書き換えてます
CJS だと ESM のみのパッケージを使うときに 動的 import にするしかなくて非同期処理にせざるを得ないです
自分で Rollup で個別に変換してたときもありましたが 依存パッケージに ESM のみが増えていくとやってられないですし
要望が多くて CJS から同期処理でインポートする手段が提供されるかと思ったりもしてましたが 結局そういうのは入らなそうですし
ESM にしても関連のところに書いたような問題は出てくるのですが CJS から ESM パッケージをインポートするのよりはマシかなというところです
ブロックスコープ内でのエクスポート問題ですが これはひとつのモジュールに色々まとめ過ぎということもあったので ブロックごとに別モジュールにします
モジュールが少ないものだと 5 個が 10 個になるのは倍なので抵抗があったりもしましたが 全体が大きくなって数百もあれば 10 や 20 増えてもたいして気になりませんし 数行しかなくても別モジュールにわけて index.js 的な部分でまとめるようにします
ブロックが if 文なのは条件によってはエクスポートが undefined ということなので宣言的になるよう export const で条件演算子で分岐する形にします
動的な require が import になることで非同期になる問題があります
config ファイルを env の名前を使って読み込むときなどは名前が静的でないので動的 import にせざるを得ません
ですが今はトップレベル await があるので 実質同期的なように初期化できます
読み込み順が変わるので場合によっては問題になることもありますが ほとんどの場合は無視できます
詳しく書くとこういうケースです
/// index.js
import a from "./a.js"
import b from "./b.js"
console.log("I")
/// a.js
console.log("A")
await import("./a2.js")
export default "A"
/// a2.js
console.log("A2")
export default "A2"
/// b.js
console.log("B")
export default "B"
結果はこうなります
A
B
A2
I
これが require だと同期処理なので a.js を最初に読み込み a2.js も同期的に読み込まれます
そのあとに b.js が読み込まれるので順番は A → A2 → B → I です
import だと a.js と b.js は並列して取得してから順番に a と b を実行しますが a.js で非同期処理が入ると b.js に進みます
index.js のインポート部分が全部終わらないと index.js の本体の処理は始まりませんが index.js がインポートするモジュールは同時に処理されます
モジュール内で完結してるなら問題ないのですが トップレベルでグローバルに影響する処理をしたり 別モジュールの関数呼び出したりしていると 実行順で期待通りに動かないこともあります
トップレベルではできるかぎり関数等を定義するだけにして処理は行わないようにして 初期化処理が必要なら使う側で init みたいな関数を呼び出してもらい実行するようしたほうがいいかもですね
残る問題はたまにしか使わない機能なので動的に import する場合です
動的インポートなのでその関数が非同期になってしまいます
完全には避けられないので ロードする処理とモジュールを使う処理を分けて 後者の処理は同期処理に保つくらいしかできないです
ただ いつ最初に使うかわからないので 結局チェックしてロードする処理を挟む可能性が常にあって あまり意味がないです
その機能を呼び出す前の段階でその機能を有効にするようなフェーズがあるのなら そこでインポートしておくという使い方はできそうです
あとは 少し面倒な点で CJS のみ対応のライブラリのインポート時にプロパティを直接 named export とみなせません
/// module.cjs
module.exports = { foo: "bar" }
/// index.js
const { foo } from "./module.cjs"
これができません
const module from "./module.cjs"
const { foo } = module
という一手間が必要です
Node.js の組み込みモジュールはソースコード上は CJS なのにプロパティを直接参照できるのでなにか方法があるのかと思ったのですが なさそうでした
組み込みモジュールだからこそ特別な対応がされているのでしょうか
WebComponents で作ったコンポーネントで現在のコンテキストを受け取りたいことがあります
コンテキストというのは React のコンテキストみたいなもので 親で持っているデータのことです
WebComponents は DOM なので親をたどるのが簡単です
connectedCallback で接続されたときに
のようにすれば ShadowRoot や その ShadowRoot を保持するホスト要素にアクセスできます
これを繰り返し期待のコンテキストを保持する要素を見つけて 見つかったらそこからデータを受け取るといいです
イメージ
これで良さそうかなと思ったのですが slot を使う場合に問題がありました
こういう構造の場合 bar-elem は foo-elem の ShadowDOM の中のどこかで slot を使って表示されます
この場合に foo-elem がコンテキストデータを持っていれば bar-elem は foo-elem からコンテキストデータを受け取りたいです
しかし getRootNode を使ってたどると bar-elem が最初に見るホスト要素は foo-elem と bar-elem の両方のホストです
foo-elem のように同じ ShadowDOM に属する祖先要素はスキップされてしまいます
これに対処するいい方法はないかなと探してると Lit ではイベントを使って親にリクエストを送り 親がコールバック関数を使って値を返す方法を使ってるようでした
その方針にしてみます
これで良さそうですが 親でコンテキストを更新したことに気づけません
コールバックで渡す値を EventTarget オブジェクトにして 受け取ったらそこにリスナをつけるなどでしょうか
動く例
https://nexpr.gitlab.io/public-pages/webcomponents-context/example.html
foo-elem がコンテキストデータとして { num: 1 } を持っています
子孫の bar-elem は foo-elem のコンテキストデータを受け取り num を表示します
ボタンを押すと num の数値が 1 ずつ増えて bar-elem に反映されます
コンテキストというのは React のコンテキストみたいなもので 親で持っているデータのことです
WebComponents は DOM なので親をたどるのが簡単です
connectedCallback で接続されたときに
const shadow_root = this.getRootNode()
const host = shadow_root.host
のようにすれば ShadowRoot や その ShadowRoot を保持するホスト要素にアクセスできます
これを繰り返し期待のコンテキストを保持する要素を見つけて 見つかったらそこからデータを受け取るといいです
イメージ
const wmap = new WeakMap()
// コンテキストを識別するためのオブジェクトを作る
// WeakMap のキーにする
// 使わないけどデバッグ時のわかりやすさのために name を持たせておく
const createCtx = (name) => {
return { name }
}
// 要素にコンテキストデータをセットする
// CustomElements のコンストラクタで実行を想定
const setCtxData = (elem, ctx, data) => {
if (!wmap.has(elem)) {
wmap.set(elem, new WeakMap())
}
const ctxs = wmap.get(elem)
if (!ctxs.has(ctx)) {
ctxs.set(ctx, { value: null })
}
const container = ctxs.get(ctx)
container.value = data
}
// 一致するコンテキストを持つ祖先を探して
// 見つかればコンテキストのデータを取得する
const findCtxData = (elem, ctx) => {
const host = elem.getRootNode().host
if (!host) return null
const container = wmap.get(elem)?.get(ctx)
if (container) return container.value
return findCtxData(host)
}
これで良さそうかなと思ったのですが slot を使う場合に問題がありました
<foo-elem>
<bar-elem></bar-elem>
</foo-elem>
こういう構造の場合 bar-elem は foo-elem の ShadowDOM の中のどこかで slot を使って表示されます
この場合に foo-elem がコンテキストデータを持っていれば bar-elem は foo-elem からコンテキストデータを受け取りたいです
しかし getRootNode を使ってたどると bar-elem が最初に見るホスト要素は foo-elem と bar-elem の両方のホストです
foo-elem のように同じ ShadowDOM に属する祖先要素はスキップされてしまいます
これに対処するいい方法はないかなと探してると Lit ではイベントを使って親にリクエストを送り 親がコールバック関数を使って値を返す方法を使ってるようでした
その方針にしてみます
const setCtxData = (elem, ctx, data) => {
elem.addEventListener("ctx", event => {
if (event.detail.ctx === ctx) {
event.stopImmediatePropagation()
event.detail.callback(data)
}
})
}
const findCtxData = (elem, ctx) => {
let data
const event = new CustomEvent("ctx", {
detail: {
ctx,
callback: (d) => {
data = d
},
},
bubbles: true,
composed: true,
})
this.dispatchEvent(event)
return data
}
これで良さそうですが 親でコンテキストを更新したことに気づけません
コールバックで渡す値を EventTarget オブジェクトにして 受け取ったらそこにリスナをつけるなどでしょうか
class Context extends EventTarget {
_value = null
get value() { return this._value }
set value(v) {
this._value = v
this.dispatchEvent(new Event("change"))
}
constructor(value) {
super()
this.value = value
}
}
const setCtxData = (elem, ctx, data) => {
const ctx_data = new ContextData(data)
elem.addEventListener("ctx", event => {
if (event.detail.ctx === ctx) {
event.stopImmediatePropagation()
event.detail.callback(ctx_data)
}
})
return ctx_data
}
const findCtxData = (elem, ctx) => {
let ctx_data
const event = new CustomEvent("ctx", {
detail: {
ctx,
callback: (d) => {
ctx_data = d
},
},
bubbles: true,
composed: true,
})
elem.dispatchEvent(event)
return ctx_data
}
const subscribeExample = (elem, ctx) => {
const ctx_data = findCtxData(elem, ctx)
ctx_data.addEventListener("change", () => {
console.log("changed", ctx_data.value)
})
}
動く例
https://nexpr.gitlab.io/public-pages/webcomponents-context/example.html
foo-elem がコンテキストデータとして { num: 1 } を持っています
子孫の bar-elem は foo-elem のコンテキストデータを受け取り num を表示します
ボタンを押すと num の数値が 1 ずつ増えて bar-elem に反映されます
input 要素に maxlength 属性をつけると文字数制限ができます
ですが IME で変換している状態では 文字数制限を超えて入力できます
確定時に超えた部分は捨てられる挙動です
これができないと日本語入力だと不便です
例えば 2 文字入力できるところに「入力」と入力したいとします
ひらがなで 「にゅうりょく」 と入力してから変換するわけですが IME の変換中も 2 文字しか入らないと 「にゅ」 までしか入力できません
ローマ字入力だと 「nyu」 ですからこの時点でもオーバーしています
React などで input を制御するときは入力文字や文字数を制御して最初から入力できないようにすることがありますが そのときに maxlength の動きのように IME で変換中は入力不可の文字も一時的に入力できるようにしたいです
IME の状態の変化は compositionstart と compositionend イベントで取得できます
一応 input イベントの isComposing プロパティでもわかりますが これだとその入力が変換中のものかはわかりますが 確定されたことが伝わらないので compositionend イベントのほうがいいです
maxlength 相当なものを作ってみました
Input1 と Input2 があり どちらも IME の変換中は最大文字数を超えられます
Input1 は最大文字数を超えた状態でも onChange を呼び出します
input イベントに近い感じで 入力があれば不正状態でも親に伝えます
Input2 ではローカルステートを用意して IME の変換中は親には変更があったことを伝えず内部のステートのみ更新します
ここでは maxlength 相当の文字数処理しかしてませんが 電話番号入力欄で確定時に数字とハイフン以外を消すようにすれば 「でんわ」 から電話番号に変換できるよう IME に登録してるユーザーを考慮することができたりします
ちなみに React などで IME の変換中に文字数オーバーや使えない文字を消しても IME は内部で入力状態を持っているので 画面には見えないだけで変換候補はちゃんと入力したものとして出てきます
しかし 変換候補を切り替えるときに 見えている文字を置き換えるために元の文字数分を消して現在の変換候補の文字を入力するので その間で無理やり書き換えると関係ない文字が消えてしまい正しく入力できなくなります (Google IME で確認)
そもそも入力自体を不可にする処理っているのですかね?
ですが IME で変換している状態では 文字数制限を超えて入力できます
確定時に超えた部分は捨てられる挙動です
これができないと日本語入力だと不便です
例えば 2 文字入力できるところに「入力」と入力したいとします
ひらがなで 「にゅうりょく」 と入力してから変換するわけですが IME の変換中も 2 文字しか入らないと 「にゅ」 までしか入力できません
ローマ字入力だと 「nyu」 ですからこの時点でもオーバーしています
React などで input を制御するときは入力文字や文字数を制御して最初から入力できないようにすることがありますが そのときに maxlength の動きのように IME で変換中は入力不可の文字も一時的に入力できるようにしたいです
IME の状態の変化は compositionstart と compositionend イベントで取得できます
一応 input イベントの isComposing プロパティでもわかりますが これだとその入力が変換中のものかはわかりますが 確定されたことが伝わらないので compositionend イベントのほうがいいです
maxlength 相当なものを作ってみました
const App = () => {
const [text1, setText1] = useState("")
const [text2, setText2] = useState("")
return (
<div>
<Input1 value={text1} onChange={setText1} max={2} />
<p>{text1}</p>
<hr/>
<Input2 value={text2} onChange={setText2} max={2} />
<p>{text2}</p>
</div>
)
}
const Input1 = ({ value, onChange, max }) => {
const [composing, setComposing] = useState(false)
return (
<input
value={value}
onChange={(event) => {
if (composing) {
onChange(event.target.value)
} else {
onChange(event.target.value.slice(0, max))
}
}}
onCompositionStart={() => {
setComposing(true)
}}
onCompositionEnd={() => {
setComposing(false)
onChange(value.slice(0, max))
}}
/>
)
}
const Input2 = ({ value, onChange, max }) => {
const [local, setLocal] = useState("")
const [composing, setComposing] = useState(false)
useEffect(() => {
setLocal(value)
}, [value])
return (
<input
value={local}
onChange={(event) => {
const v = event.target.value
if (composing) {
setLocal(v)
} else {
const fixed = v.slice(0, max)
setLocal(fixed)
onChange(fixed)
}
}}
onCompositionStart={() => {
setComposing(true)
}}
onCompositionEnd={() => {
setComposing(false)
const fixed = local.slice(0, max)
setLocal(fixed)
onChange(fixed)
}}
/>
)
}
Input1 と Input2 があり どちらも IME の変換中は最大文字数を超えられます
Input1 は最大文字数を超えた状態でも onChange を呼び出します
input イベントに近い感じで 入力があれば不正状態でも親に伝えます
Input2 ではローカルステートを用意して IME の変換中は親には変更があったことを伝えず内部のステートのみ更新します
ここでは maxlength 相当の文字数処理しかしてませんが 電話番号入力欄で確定時に数字とハイフン以外を消すようにすれば 「でんわ」 から電話番号に変換できるよう IME に登録してるユーザーを考慮することができたりします
ちなみに React などで IME の変換中に文字数オーバーや使えない文字を消しても IME は内部で入力状態を持っているので 画面には見えないだけで変換候補はちゃんと入力したものとして出てきます
しかし 変換候補を切り替えるときに 見えている文字を置き換えるために元の文字数分を消して現在の変換候補の文字を入力するので その間で無理やり書き換えると関係ない文字が消えてしまい正しく入力できなくなります (Google IME で確認)
そもそも入力自体を不可にする処理っているのですかね?
ダイアログで入力するものがあるとき 入力の途中でダイアログの裏側にある画面をみたいことってありますよね
でも裏側を見るためにダイアログを一旦閉じたら入力中の内容が消える場合が多いです
対処として別タブで同じページを開くこともありますが 面倒です
手間なく簡単に見れるようにしたいです
jQuery 時代によく見かけたものだと Windows のウィンドウみたいな感じでヘッダーをドラッグして動かせるものがありました
悪くはないですが ダイアログは自由な位置じゃなくて決まった位置にいて欲しい気持ちがあります
また 動かせても背景が暗いままだと裏側の文字が見づらいです
ということで 思ったのが小さくして端の方に持っていきたいというものです
Youtube の動画で右下で小さくして再生できたりしますが あんな感じ
その状態で必要な情報を見たりコピーしたりして またダイアログを表示させて入力します
固定で右下に持ってきたら 見たいものが右下にあると困るので 一応端に持ってきた状態ならドラッグで動かせたほうがいいかもしれません
そんな感じで試しに作ってみたのがこれです
https://nexpr.gitlab.io/public-pages/floatable-dialog/example.html
下の方にあるボタンを押すと ダイアログが開きます
ダイアログヘッダーの右側の小さくしそうなアイコンのクリックで縮めます
小さくしたらドラッグで動かせます
広がりそうなアイコンをクリックしたらダイアログを復元します
試しに くらいのつもりで 2, 3 時間くらいで簡単に作ったものなので 画面外までドラッグできたり色々問題もありますが 思ったよりいいかもしれません
その後 もう少し豪華なサンプルも用意しました
ダイアログを通して要素の追加・編集ができるので 実際に裏側のものをコピーして入力などができます
https://nexpr.gitlab.io/public-pages/floatable-dialog/rich-example.html
でも裏側を見るためにダイアログを一旦閉じたら入力中の内容が消える場合が多いです
対処として別タブで同じページを開くこともありますが 面倒です
手間なく簡単に見れるようにしたいです
jQuery 時代によく見かけたものだと Windows のウィンドウみたいな感じでヘッダーをドラッグして動かせるものがありました
悪くはないですが ダイアログは自由な位置じゃなくて決まった位置にいて欲しい気持ちがあります
また 動かせても背景が暗いままだと裏側の文字が見づらいです
ということで 思ったのが小さくして端の方に持っていきたいというものです
Youtube の動画で右下で小さくして再生できたりしますが あんな感じ
その状態で必要な情報を見たりコピーしたりして またダイアログを表示させて入力します
固定で右下に持ってきたら 見たいものが右下にあると困るので 一応端に持ってきた状態ならドラッグで動かせたほうがいいかもしれません
そんな感じで試しに作ってみたのがこれです
https://nexpr.gitlab.io/public-pages/floatable-dialog/example.html
下の方にあるボタンを押すと ダイアログが開きます
ダイアログヘッダーの右側の小さくしそうなアイコンのクリックで縮めます
小さくしたらドラッグで動かせます
広がりそうなアイコンをクリックしたらダイアログを復元します
試しに くらいのつもりで 2, 3 時間くらいで簡単に作ったものなので 画面外までドラッグできたり色々問題もありますが 思ったよりいいかもしれません
その後 もう少し豪華なサンプルも用意しました
ダイアログを通して要素の追加・編集ができるので 実際に裏側のものをコピーして入力などができます
https://nexpr.gitlab.io/public-pages/floatable-dialog/rich-example.html
Lit の Directive は LitElement と統合されてないので Directive からホストのカスタム要素にアクセスできません
一応 Part にアクセスすれば RenderOptions の値を options プロパティで持ってるのでアクセスできるといえばできるのですが Part は公開されていないため __part に無理やりアクセスするような方法になります
minify で名前が変わるかもしれませんし あまりアクセスしたいところではないです
自分で持たせようにも Directive のインスタンスを作る処理に手を加えることはできないです
render の処理の中で directive の関数を呼び出すときに都度 this を渡せば Directive の render メソッドの引数として渡せますが 全てにそれを書くのは避けたいです
自動で this を追加で渡すようにラップした関数をプロパティで持たせておくこともできます
constructor でこういう感じのことをします
これだと Directive の render メソッドの第一引数で host を受け取ることになります
Directive のインスタンスのプロパティに入ってるほうが自然な気がするので directive 関数を呼び出して directive の関数を作るところでクラスも作ります
インスタンスごとに異なるクラスになります
このクラスを作るときにスコープ的にホストのカスタム要素を見れるようにして これを参照させます
コードにするとこんな感じになりました
example-element1 と example-element2 が使用例です
2 の方では ボタンを押すとプロパティを更新して表示内容を切り替えています
下の div の表示の有無が切り替わります
一旦消えてから DOM が再作成されるときに Directive のインスタンスも作り直されます
これを使えば画面の更新時にホストの要素も更新する Directive が作れそうです
一応 Part にアクセスすれば RenderOptions の値を options プロパティで持ってるのでアクセスできるといえばできるのですが Part は公開されていないため __part に無理やりアクセスするような方法になります
minify で名前が変わるかもしれませんし あまりアクセスしたいところではないです
自分で持たせようにも Directive のインスタンスを作る処理に手を加えることはできないです
render の処理の中で directive の関数を呼び出すときに都度 this を渡せば Directive の render メソッドの引数として渡せますが 全てにそれを書くのは避けたいです
render() {
return html`<div>${directive1(this, foo, bar)}</div>`
}
自動で this を追加で渡すようにラップした関数をプロパティで持たせておくこともできます
constructor でこういう感じのことをします
this.directive1 = (...args) => directive1(this, ...args)
これだと Directive の render メソッドの第一引数で host を受け取ることになります
Directive のインスタンスのプロパティに入ってるほうが自然な気がするので directive 関数を呼び出して directive の関数を作るところでクラスも作ります
インスタンスごとに異なるクラスになります
このクラスを作るときにスコープ的にホストのカスタム要素を見れるようにして これを参照させます
コードにするとこんな感じになりました
import { html, LitElement } from "lit"
import { directive, Directive } from "lit/directive.js"
const customElementDirective = (element, Directive) => {
return directive(
class extends Directive {
host = element
}
)
}
class Dire extends Directive {
host = null
constructor() {
super()
console.log("Directive created")
}
render(name) {
console.log("Directive rendered", this.host, name)
return "a"
}
}
class ExampleElement1 extends LitElement {
d = customElementDirective(this, Dire)
render() {
return html`
<div class=${this.d("A")}>${this.d("B")}</div>
`
}
}
class ExampleElement2 extends LitElement {
static properties = {
n: {}
}
constructor() {
super()
this.d = customElementDirective(this, Dire)
this.n = 0
}
render() {
return html`
<button @click=${() => this.n++}>${this.n}</button>
<div>${this.d("A")}</div>
${this.n % 2 === 0
? html`<div class=${this.d("B")}>${this.d("C")}</div>`
: null
}
`
}
}
customElements.define("example-element1", ExampleElement1)
customElements.define("example-element2", ExampleElement2)
example-element1 と example-element2 が使用例です
2 の方では ボタンを押すとプロパティを更新して表示内容を切り替えています
下の div の表示の有無が切り替わります
一旦消えてから DOM が再作成されるときに Directive のインスタンスも作り直されます
これを使えば画面の更新時にホストの要素も更新する Directive が作れそうです
前記事を書いていて関数のかわりにコンポーネントで渡すメリットもあるなと思ったので
まず前提ですが 関数を受け取り実行して結果を内部で使うコンポーネントがあります
これの嫌なところは useEffect が入るところです
関数の代わりにコンポーネントで渡すと コンポーネントに渡した関数が呼び出されてイベントベースな動きにできます
GetValue の中に useEffect はありますが これはそれをするためだけのコンポーネントです
Component2 の中に useEffect が存在しなくなるのが良いところです
元の Component1 がほぼ何もしないコンポーネントなのでこれだとあまり違いは感じられませんが もっと色々し始めると useEffect での呼び出しが減るのは少し助かります
まず前提ですが 関数を受け取り実行して結果を内部で使うコンポーネントがあります
const Component1 = ({ getValue }) => {
const [state, setState] = useState()
useEffect(() => {
getValue().then(value => setState(value))
}, [getValue])
return (
<div>{state}</div>
)
}
const getValue = () => {
return new Promise(resolve => setTimeout(resolve, 100, "VALUE"))
}
これの嫌なところは useEffect が入るところです
関数の代わりにコンポーネントで渡すと コンポーネントに渡した関数が呼び出されてイベントベースな動きにできます
const Component2 = ({ GetValue }) => {
const [state, setState] = useState()
return (
<>
<GetValue onValue={value => setState(value)} />
<div>{state}</div>
</>
)
}
const GetValue = ({ onValue }) => {
useEffect(() => {
getValue().then(value => onValue(value))
}, [])
return null
}
GetValue の中に useEffect はありますが これはそれをするためだけのコンポーネントです
Component2 の中に useEffect が存在しなくなるのが良いところです
元の Component1 がほぼ何もしないコンポーネントなのでこれだとあまり違いは感じられませんが もっと色々し始めると useEffect での呼び出しが減るのは少し助かります
Lit の 3.0 が正式リリースされたので久々に Lit を使っています
React で CSS-in-JS ライブラリを使っていると Lit でスタイルを適用したいときに少し不便です
一応 styleMap という directive があるのですが 単純に style 属性を設定するものです
最近ブラウザで使えるようになったネストするセレクタは書けません
やりたいものはこういうのです
div の class のところでスタイルを書いて 自動生成されるクラス名が class に渡されて設定されるというものです
だいたい Emotion です
ただ そこまで複雑なことはしなくてもいいです
ネストはブラウザ側の機能で実現できるので 単純にクラス名を作って設定するだけでいいです
それなら自分で簡単にできるかなと試してました
LitElement のクラスでは styles という静的プロパティを用意しておくことで 自動でそのスタイルをコンポーネントに適用してくれる機能があります
しかし こういう動的に更新するものには使えないです
なので自分で adoptedStyleSheets を追加します
コンストラクタのタイミングでは ShadowRoot を作ってくれないので createRenderRoot をオーバーライドして ここで追加します
あとは style 関数が呼び出されたタイミングで中身を更新します
これで動くようになりました
ただこれだと最後に呼び出された style 関数の分しかスタイルを保持していません
replaceSync の代わりに insertRules にすれば追加できますが 増えていく一方です
同じ場所の更新ならルールを更新できるといいのですけど
テンプレートリテラルの機能を使ってみようかとも思いました
しかし あくまでソースコード上の同じ場所ということしかわかりません
CSSStyleSheet が CustomElement ごとなので 別インスタンスと混ざることはないですが map で繰り返すようなケースは対応できません
Lit の directive が使えるかと思いましたが directive は lit-html 側の機能なので LitElement と統合されていないです
CustomElement の参照は得られず 対応する Part の更新だけに焦点が当てられています
また Part が消えた場合の処理を記述する方法も用意されていません
いい方法がみつからないので とりあえず全部更新する方法にしました
update をオーバーライドすれば render の前後に処理を入れられるので 更新時に使ったルールだけを残し 他は削除します
スタイルの更新のための処理が増えてきたので 間にクラスを設けることにしました
このクラスを使います
複数の style を使って map の中でも使うようにしています
また再レンダリングを兼ねて input の入力を全体で同期するようにしました
とりあえず動くようになりましたが render の呼び出しのたびに style で作るルールは全部新規に作り 古いのは消すという処理です
あんまりパフォーマンス的に優れてそうにないです
でもこれ以上がんばるならもう Emotion 使ったほうがいいような気もしてきます
ただ Emotion にしても同じルールが存在するかを確認して差分更新をするのでそこまで改善されるのかは疑問です
簡単にできそうで効率良くなるかもってところだと やっぱりルールの更新方法でしょうか
差分更新はがんばらないにしても もう少しなんとかできるかもと思います
CSSStyleSheet の削除って index 指定でしか消せず ひとつひとつ index を取得してから消すのって効率悪そうです
ルールの全置き換えなら 最初から新規の CSSStyleSheet を作って それと置き換えてしまう方がよいかもしれないです
それか 配列でルールの文字列を保持して最後に replaceSync を使うかでしょうか
React で CSS-in-JS ライブラリを使っていると Lit でスタイルを適用したいときに少し不便です
一応 styleMap という directive があるのですが 単純に style 属性を設定するものです
最近ブラウザで使えるようになったネストするセレクタは書けません
やりたいものはこういうのです
customElements.define("foo-bar", class extends LitElement {
render() {
return html`
<div
class=${style`
border: 2px solid #aaa;
padding: 12px;
:is(input) {
padding: 4px;
}
`}
>
<label>name</label>
<input>
</div>
`
}
})
div の class のところでスタイルを書いて 自動生成されるクラス名が class に渡されて設定されるというものです
だいたい Emotion です
ただ そこまで複雑なことはしなくてもいいです
ネストはブラウザ側の機能で実現できるので 単純にクラス名を作って設定するだけでいいです
それなら自分で簡単にできるかなと試してました
LitElement のクラスでは styles という静的プロパティを用意しておくことで 自動でそのスタイルをコンポーネントに適用してくれる機能があります
しかし こういう動的に更新するものには使えないです
なので自分で adoptedStyleSheets を追加します
コンストラクタのタイミングでは ShadowRoot を作ってくれないので createRenderRoot をオーバーライドして ここで追加します
あとは style 関数が呼び出されたタイミングで中身を更新します
customElements.define("foo-bar", class extends LitElement {
constructor() {
super()
this._cssss = new CSSStyleSheet()
}
createRenderRoot() {
const root = super.createRenderRoot()
root.adoptedStyleSheets.push(this._cssss)
return root
}
style(template, ...values) {
const class_name = "c"
this._cssss.replaceSync(`.${class_name} {${String.raw(template, ...values)}}`)
return class_name
}
render() {
return html`
<div
class=${this.style`
border: 2px solid #aaa;
padding: 12px;
:is(input) {
padding: 4px;
}
`}
>
<label>name</label>
<input>
</div>
`
}
})
これで動くようになりました
ただこれだと最後に呼び出された style 関数の分しかスタイルを保持していません
replaceSync の代わりに insertRules にすれば追加できますが 増えていく一方です
同じ場所の更新ならルールを更新できるといいのですけど
テンプレートリテラルの機能を使ってみようかとも思いました
しかし あくまでソースコード上の同じ場所ということしかわかりません
CSSStyleSheet が CustomElement ごとなので 別インスタンスと混ざることはないですが map で繰り返すようなケースは対応できません
Lit の directive が使えるかと思いましたが directive は lit-html 側の機能なので LitElement と統合されていないです
CustomElement の参照は得られず 対応する Part の更新だけに焦点が当てられています
また Part が消えた場合の処理を記述する方法も用意されていません
いい方法がみつからないので とりあえず全部更新する方法にしました
update をオーバーライドすれば render の前後に処理を入れられるので 更新時に使ったルールだけを残し 他は削除します
スタイルの更新のための処理が増えてきたので 間にクラスを設けることにしました
class StylitElement extends LitElement {
constructor() {
super()
this._cssss = new CSSStyleSheet()
this._style_num = 0
this._style_used_rules = new Set()
}
createRenderRoot() {
const root = super.createRenderRoot()
root.adoptedStyleSheets.push(this._cssss)
return root
}
style(template, ...values) {
const class_name = "c" + (this._style_num++).toString(36)
const index = this._cssss.insertRule(`.${class_name} {${String.raw(template, ...values)}}`)
this._style_used_rules.add(this._cssss.cssRules.item(index))
return class_name
}
update() {
this._style_num = 0
this._style_used_rules.clear()
super.update()
const rules = [...this._cssss.cssRules]
for (const rule of rules.toReversed()) {
if (!this._style_used_rules.has(rule)) {
const index = rules.indexOf(rule)
this._cssss.deleteRule(index)
}
}
}
}
このクラスを使います
複数の style を使って map の中でも使うようにしています
また再レンダリングを兼ねて input の入力を全体で同期するようにしました
customElements.define("foo-bar", class extends StylitElement {
static properties = {
name: {},
}
constructor() {
super()
this.name = ""
}
render() {
return html`
<div class=${this.style`display: flex; flex-flow: column; gap: 10px;`}>
<div
class=${this.style`
border: 2px solid #aaa;
padding: 12px;
:is(input) {
padding: 4px;
}
`}
>
<label>name</label>
<input .value=${this.name} @input=${event => this.name = event.target.value}>
</div>
<div
class=${this.style`
border: 2px solid #f8d;
padding: 20px;
:is(input) {
padding: 4px;
}
`}
>
<label>name</label>
<input .value=${this.name} @input=${event => this.name = event.target.value}>
</div>
${["#f00", "#0f0", "#00f"].map(color => {
return html`
<div
class=${this.style`
border: 2px solid ${color};
padding: 12px;
:is(input) {
padding: 4px;
}
`}
>
<label>name</label>
<input .value=${this.name} @input=${event => this.name = event.target.value}>
</div>
`
})}
</div>
`
}
})
とりあえず動くようになりましたが render の呼び出しのたびに style で作るルールは全部新規に作り 古いのは消すという処理です
あんまりパフォーマンス的に優れてそうにないです
でもこれ以上がんばるならもう Emotion 使ったほうがいいような気もしてきます
ただ Emotion にしても同じルールが存在するかを確認して差分更新をするのでそこまで改善されるのかは疑問です
簡単にできそうで効率良くなるかもってところだと やっぱりルールの更新方法でしょうか
差分更新はがんばらないにしても もう少しなんとかできるかもと思います
CSSStyleSheet の削除って index 指定でしか消せず ひとつひとつ index を取得してから消すのって効率悪そうです
ルールの全置き換えなら 最初から新規の CSSStyleSheet を作って それと置き換えてしまう方がよいかもしれないです
それか 配列でルールの文字列を保持して最後に replaceSync を使うかでしょうか
先日書いた内容で 複雑で難しそうだし もっと楽なやり方で解決したというのがあったのですが やっぱり気になったので元々やろうとした方法でやってみたという話です
複数の対象が配列に入っていて 非同期処理で並列に処理します
それぞれの処理では 対象に基づいてデータを外部から取得して そのデータを使ってなにかするというものです
最後のなにかするというところはどうでもよくて 今回注目するのは取得部分です
楽な方法では 先に全ての対象で必要になるデータの一覧を作ります
それらをまとめて取ってきて終わったら各自の処理を行うというものです
これでもいいのですが 1 つ取得に時間がかかると それに依存しないものの処理の開始も遅れます
そうならないように各処理の中で必要なデータを取得するようにします
必要なデータは複数の対象で共有してるケースがあります
そういうときに 2 回取得しないようにしたいです
fetch やモジュールならキャッシュされてるから別にいいような気もしますが そうでもない場合も考えて効率化します
面倒なのは依存関係があることです
A には B, C が必要というものです
A の取得開始時に C がすでに取得を開始してるかもしれません
そういうときには C の取得は行わず すでに行われてる取得処理を待って それが終わったらその取得できたものを参照したいです
また B には A が必要ということもあります
そうなると循環参照になります
A は B の完了を待って B は A の完了を待つので いつまで経っても終わらないということになります
そういうことがあるので複雑なのですよね
嬉しいことに今回は依存関係は取得する前からわかっています
依存関係全部の取得開始を同期的に行えるので 取得開始は他の並列する処理と混在しません
コードで書くとこういうものです
とりあえずこんな感じでできました
試した感じは問題なく動いてそうです
確認しやすいように load はディレイを挟んで引数を返すものにしてます
デバッグを兼ねて 結果として出力するものは再帰の呼び出しの構造になってます
呼び出し順で結果が違うので実際に使うなら名前と取得したデータのオブジェクトにするなど必要です
複数の対象が配列に入っていて 非同期処理で並列に処理します
それぞれの処理では 対象に基づいてデータを外部から取得して そのデータを使ってなにかするというものです
最後のなにかするというところはどうでもよくて 今回注目するのは取得部分です
楽な方法では 先に全ての対象で必要になるデータの一覧を作ります
それらをまとめて取ってきて終わったら各自の処理を行うというものです
これでもいいのですが 1 つ取得に時間がかかると それに依存しないものの処理の開始も遅れます
そうならないように各処理の中で必要なデータを取得するようにします
必要なデータは複数の対象で共有してるケースがあります
そういうときに 2 回取得しないようにしたいです
fetch やモジュールならキャッシュされてるから別にいいような気もしますが そうでもない場合も考えて効率化します
面倒なのは依存関係があることです
A には B, C が必要というものです
A の取得開始時に C がすでに取得を開始してるかもしれません
そういうときには C の取得は行わず すでに行われてる取得処理を待って それが終わったらその取得できたものを参照したいです
また B には A が必要ということもあります
そうなると循環参照になります
A は B の完了を待って B は A の完了を待つので いつまで経っても終わらないということになります
そういうことがあるので複雑なのですよね
嬉しいことに今回は依存関係は取得する前からわかっています
依存関係全部の取得開始を同期的に行えるので 取得開始は他の並列する処理と混在しません
コードで書くとこういうものです
const data = ["a", "b", "a", "c", "b", "a"]
const deps = {
a: ["b", "c"],
b: ["a"],
}
const load = (item) => {
return fetch(`http://localhost/${item}`).then(res => res.json())
}
const loadWithDeps = (item) => {
// ココの処理
}
const processItem = (item) => {
return loadWithDeps(item).then(console.log)
}
data.map(item => {
return processItem(item).catch(console.error)
})
とりあえずこんな感じでできました
試した感じは問題なく動いてそうです
確認しやすいように load はディレイを挟んで引数を返すものにしてます
const global_promises = {}
const load = (item) => {
return new Promise(r => setTimeout(r, 10, item))
}
const loadWithDeps = (item) => {
if (global_promises[item]) return global_promises[item]
const promises = {}
const resolveDeps = (item) => {
if (global_promises[item]) return global_promises[item]
if (promises[item]) return promises[item]
promises[item] = load(item)
const dep_items = deps[item] || []
const dep_promises = dep_items.map(resolveDeps)
return global_promises[item] = Promise.all([promises[item], ...dep_promises])
}
return resolveDeps(item)
}
const processItem = (item) => {
return loadWithDeps(item).then(console.log)
}
const shuffle = (arr) => {
const a = arr.slice()
for (let i = 0; i < arr.length; i++) {
const rand = ~~(Math.random() * a.length)
const tmp = a[i]
a[i] = a[rand]
a[rand] = tmp
}
return a
}
const data = shuffle(["a", "b", "a", "c", "b", "a"])
const deps = {
"a": ["b", "c"],
"b": ["a"],
}
console.log(data)
data.map(item => {
return processItem(item).catch(console.error)
})
デバッグを兼ねて 結果として出力するものは再帰の呼び出しの構造になってます
呼び出し順で結果が違うので実際に使うなら名前と取得したデータのオブジェクトにするなど必要です
前記事の続きです
重複行を削除して同じ行は 1 回だけにしたいですが Linux コマンドだと sort コマンドが必要です
ソートするのは無駄だなと思ってソートしないものを作ってみたらソートするより遅かったです
sort コマンドでのソートはマルチスレッド化されていることや自作の方は Python のコードという理由もありますが実行時間ではソートしたほうが速くなっていました
無駄な処理があっても速いのならソートすればいいとなるのですが 本来の並びが維持されない問題があります
ググっても並びをもとに戻す良さそうな方法が見つからないということもあり コマンドで頑張るよりは Python で作ったものを使えばいいかなと思ってます
ただ ソートしてるのより速度で負けてるのが気になるところです
Python みたいなスクリプト言語ではなくネイティブな言語ならもっと速くなりそうということで別言語でも作ってみることにしました
そうは言っても C/C++ 言語は使いたくないです
今のコードが Python なので 構文が似てる Nim にすることにしました
まずは元の Python のコード
Nim 版
似てはいるものの 細かいところは結構違っていて Python からなら移植が楽かというと そうとも言い切れないくらいでした
慣れていて これはこう書き換えるというのが頭に入ってるならともかく 初心者がやってみるには結構時間がかかるものだと思います
一応以前にも少し使ったことのある言語なのですが ほぼ覚えてなくて調べながら書いてました
それに Nim はちょうど今月の頭に v2.0 のリリースがされて色々変更があったようです
std/os にあった関数が std/cmdline などに移動していました
https://nim-lang.org/blog/2023/08/01/nim-v20-released.html
あと一番困るのがインデントはスペースのみという制限です
1 種類だけに制限するならインデントのためにある文字のタブにすべきだと思いますね
タブと違ってスペースは可変じゃないのでアクセシビリティなどでも劣ってると言われることが増えているのにこの考えなのは残念です
コンパイル前の処理として全 .nim ファイルのタブをスペースに置換した一時ファイルを作ってそれらを使ってコンパイルするよう一手間必要になります
それは置いておき実行時間の比較です
遅っ
Python でも
くらいだったのですけど
ネイティブ系の言語で遅いときはだいたい最適化オプションがないからだと思うので探してみます
のようにしてビルドすれば リリース用ビルドで速度に最適化できるそうです
これでビルドし直して試してみます
Python よりも速くなりました
sort + uniq よりも速いですね
ただ条件としてほとんどが重複行という前提があります
重複率を下げるため出力条件を変更してみました
これで実行してみると Nim だと 19 ~ 20 秒です
ソートを行う方は 21 秒だったので 差は縮まったけど一応は勝ってると思ってました
ただやっぱりソートをする方は実行するごとに速くなって 21 → 10 → 7.1 → 6.8 秒まで速くなりました
これ以上は速くならないようです
読み込んでるファイルは同じなのでファイルのキャッシュなら Nim 側の時点でキャッシュされてるはずですし そっちでも高速化しそうですがそんなことはなかったです
どういう仕組みなんでしょうね
重複行を削除して同じ行は 1 回だけにしたいですが Linux コマンドだと sort コマンドが必要です
ソートするのは無駄だなと思ってソートしないものを作ってみたらソートするより遅かったです
sort コマンドでのソートはマルチスレッド化されていることや自作の方は Python のコードという理由もありますが実行時間ではソートしたほうが速くなっていました
無駄な処理があっても速いのならソートすればいいとなるのですが 本来の並びが維持されない問題があります
ググっても並びをもとに戻す良さそうな方法が見つからないということもあり コマンドで頑張るよりは Python で作ったものを使えばいいかなと思ってます
ただ ソートしてるのより速度で負けてるのが気になるところです
Python みたいなスクリプト言語ではなくネイティブな言語ならもっと速くなりそうということで別言語でも作ってみることにしました
そうは言っても C/C++ 言語は使いたくないです
今のコードが Python なので 構文が似てる Nim にすることにしました
まずは元の Python のコード
import sys
if len(sys.argv) < 2:
exit()
filename = sys.argv[1]
with open(filename) as file:
set = set()
while line := file.readline():
line = line.rstrip("\n")
if line not in set:
set.add(line)
print(line)
Nim 版
import std/sets
import std/cmdline
let params = commandLineParams()
if params.len == 0:
quit()
let filename = params[0]
block:
let file = open(filename)
defer: file.close
var set = HashSet[string]()
var line: string
while file.readLine(line):
if line notin set:
set.incl(line)
echo line
似てはいるものの 細かいところは結構違っていて Python からなら移植が楽かというと そうとも言い切れないくらいでした
慣れていて これはこう書き換えるというのが頭に入ってるならともかく 初心者がやってみるには結構時間がかかるものだと思います
一応以前にも少し使ったことのある言語なのですが ほぼ覚えてなくて調べながら書いてました
それに Nim はちょうど今月の頭に v2.0 のリリースがされて色々変更があったようです
std/os にあった関数が std/cmdline などに移動していました
https://nim-lang.org/blog/2023/08/01/nim-v20-released.html
あと一番困るのがインデントはスペースのみという制限です
1 種類だけに制限するならインデントのためにある文字のタブにすべきだと思いますね
タブと違ってスペースは可変じゃないのでアクセシビリティなどでも劣ってると言われることが増えているのにこの考えなのは残念です
コンパイル前の処理として全 .nim ファイルのタブをスペースに置換した一時ファイルを作ってそれらを使ってコンパイルするよう一手間必要になります
それは置いておき実行時間の比較です
[root@552d14e2dbe8 n]# time ./a data.txt
aaa
bbb
ccc
real 0m34.028s
user 0m33.867s
sys 0m0.160s
[root@552d14e2dbe8 n]# time ./a data.txt
aaa
bbb
ccc
real 0m33.575s
user 0m33.438s
sys 0m0.136s
遅っ
Python でも
real 0m12.368s
user 0m12.249s
sys 0m0.110s
くらいだったのですけど
ネイティブ系の言語で遅いときはだいたい最適化オプションがないからだと思うので探してみます
nim c --d:release --opt:speed a.nim
のようにしてビルドすれば リリース用ビルドで速度に最適化できるそうです
これでビルドし直して試してみます
[root@552d14e2dbe8 n]# time ./a data.txt
aaa
bbb
ccc
real 0m5.737s
user 0m5.697s
sys 0m0.040s
[root@552d14e2dbe8 n]# time ./a data.txt
aaa
bbb
ccc
real 0m5.809s
user 0m5.749s
sys 0m0.060s
Python よりも速くなりました
sort + uniq よりも速いですね
ただ条件としてほとんどが重複行という前提があります
重複率を下げるため出力条件を変更してみました
for i in range(50 * 1000 * 1000):
print(i % 1000000)
これで実行してみると Nim だと 19 ~ 20 秒です
ソートを行う方は 21 秒だったので 差は縮まったけど一応は勝ってると思ってました
ただやっぱりソートをする方は実行するごとに速くなって 21 → 10 → 7.1 → 6.8 秒まで速くなりました
これ以上は速くならないようです
読み込んでるファイルは同じなのでファイルのキャッシュなら Nim 側の時点でキャッシュされてるはずですし そっちでも高速化しそうですがそんなことはなかったです
どういう仕組みなんでしょうね
foo
bar
foo
foo
bar
bar
foo
みたいな中身のファイルがあったときに重複する行を削除して
foo
bar
だけにしたいです
Linux なら uniq コマンドが使えます
ただし uniq コマンドが除外するのは連続する重複する行なので今回みたいなケースではソートも行う必要があります
sort file.txt | uniq
でもこれってソートが無駄に思います
重複行を除去するだけなら一旦並び替えなくても 1 行ずつ読み取っていくだけでもできるはずです
あと 並びが替わってしまうので本来の並び順でほしい場合には使えません
特にちょうど今 重複除去したいデータはファイルは大きめですがほとんどが重複で最終的には 100 行にも満たないものです
1 行ずつ見ていくプログラムを作ったほうが速く処理できたりしそうです
ということで
import sys
if len(sys.argv) < 2:
exit()
filename = sys.argv[1]
with open(filename) as file:
set = set()
while line := file.readline():
line = line.rstrip("\n")
if line not in set:
set.add(line)
print(line)
というコードを用意しました
全件のソートという重そうな処理をスキップするわけですし スクリプト言語の Python ですがこっちのほうが速いのではないでしょうか
試してみます
ほとんどの行が重複するデータを作ります
values = ["a", "b", "c"]
values_len = len(values)
for i in range(10 * 1000 * 1000):
value = values[i % values_len]
print(value)
1000 万行で a,b,c のいずれかが入ってます
各行に改行文字も入るのでファイルサイズは 20MB になりました
それぞれの処理速度を比べてみます
[root@552d14e2dbe8 i]# python3 gen.py > data.txt
[root@552d14e2dbe8 i]# time python3 uniq.py data.txt
a
b
c
real 0m2.161s
user 0m2.160s
sys 0m0.000s
[root@552d14e2dbe8 i]# time ( sort data.txt | uniq )
a
b
c
real 0m1.242s
user 0m5.444s
sys 0m0.533s
ソートしたほうが速くなってますね……
user が real を超えているのでソートはマルチスレッドで処理しているのでしょうけど それにしても速すぎません?
数回実行してもだいたい同じような速度でした
1 行ずつ読み取って set に入ってるかチェックするだけなので Python でも十分な速度だと思いましたが 結構遅いのでしょうか?
set のハッシュ値計算や末尾の改行を除去する部分が遅め?
データ量を変えてもう 1 パターン試してみます
20MB より小さくしても 十分高速で速度を気にする必要もなさそうなので もっとサイズを大きくします
values = ["aaa", "bbb", "ccc"]
values_len = len(values)
for i in range(50 * 1000 * 1000):
value = values[i % values_len]
print(value)
5000 万行で 各行の文字数も増やしました
ファイルサイズは 191MB です
[root@198ac3e26189 opt]# time python3 uniq.py out.txt
aaa
bbb
ccc
real 0m12.368s
user 0m12.249s
sys 0m0.110s
[root@198ac3e26189 opt]# time ( sort out.txt | uniq )
aaa
bbb
ccc
real 0m8.984s
user 0m31.583s
sys 0m3.061s
これでもソートするほうが速いですね
ところで最初は遅めで何度か実行すると速くなりました
ブラウザと違って内部で情報を保持しないので 2 回目以降で劇的に速度が変わることはなさそうに思うのですけど
OS 側のファイルキャッシュ都合とかでしょうか
間で特別なことはせず単純に連続して実行した結果です
[root@198ac3e26189 opt]# time ( sort out.txt | uniq )
aaa
bbb
ccc
real 0m30.542s
user 1m16.845s
sys 0m13.606s
[root@198ac3e26189 opt]# time ( sort out.txt | uniq )
aaa
bbb
ccc
real 0m21.338s
user 0m46.432s
sys 0m15.657s
[root@198ac3e26189 opt]# time ( sort out.txt | uniq )
aaa
bbb
ccc
real 0m12.536s
user 0m32.160s
sys 0m7.133s
[root@198ac3e26189 opt]# time ( sort out.txt | uniq )
aaa
bbb
ccc
real 0m9.317s
user 0m30.287s
sys 0m3.861s
[root@198ac3e26189 opt]# time ( sort out.txt | uniq )
aaa
bbb
ccc
real 0m8.795s
user 0m31.538s
sys 0m2.983s
とりあえず ソートしても十分に速いのであまり気にしなくて良さそうです
並列処理前提なのでコアが 1 つしかない VM 環境とかだと話は変わってきそうですけど
また sort -u にすればソート時に重複する行を排除してくれて結果が同じになります
こっちのほうが速いかなと思いましたが 特に変わらないようでした
残る問題は並びが変わってしまうことです
uniq で得られる各行を元ファイルから探索して行数をもとに並び替えというのは面倒そうです
それに重複率が低い場合はほとんどの行を探索することになってここで時間がかかりそうです
コマンドのオプションを見てもいい感じにできそうなものがなかったですが いい方法ないのでしょうか
なんとなく思いつきで作ったもの
1..x で 1 から始まる連番のイテレータを作る
終わりはなく無限に続く
1 以外からでも始めれる
見た目はそれっぽいけど 中身はこんなのなので実用性はない
1..x で 1 から始まる連番のイテレータを作る
終わりはなく無限に続く
for (const n of 1..x) {
if (n > 5) break
console.log(n)
}
// 1
// 2
// 3
// 4
// 5
1 以外からでも始めれる
const it = 123..x
it.next()
// {value: 123, done: false}
it.next()
// {value: 124, done: false}
it.next()
// {value: 125, done: false}
見た目はそれっぽいけど 中身はこんなのなので実用性はない
Object.defineProperty(Number.prototype, "x", { get: function* () { for (let i = +this; ; i++) yield i } })
最近 WSL 1.0 がリリースされたというニュースをよく見かけました
WSL1 / WSL2 とは別の区分で WSL の仕組み自体のバージョンがあるようで ストア版が 1.0 になったそうです
最初は特に興味もなく ストア版というと勝手にアップデートされて Windows Terminal みたいな問題起きそうとかそういうイメージでした
調べてみると ただインストールの方法が違うだけでなく追加機能が色々あるようです
また これまでの OS 組み込みの方は廃止はされないものの 積極的に開発されるものはストア版のようです
これまでの WSL は WSL1 みたいな扱いになるってことでしょうか
それなら気が向いたときに切り替えておこうかなくらいに考えてましたが systemd が使えるという情報を見つけました
WSL にしても Docker コンテナにしても systemd が使えないのはとても不便だったので これはすごく良さそうです
ということで早速切り替えました
やることは KB5020030 をインストールして 「wsl --update」 コマンドを実行するだけです
今のところはこのアップデートは自動インストールされないようで Windows Update の画面でオプションとして並んでいます
これを手動でインストールしてから 「wsl --update」 を実行します
これまで全然カーネルのアップデートをしていないと複数回の実行が必要になることもあるそうです
切り替えが終わると 「wsl --version」 コマンドでバージョンが見れるようになります
古いバージョンだと 「--version」 というオプションはないので無効なオプションというエラーメッセージと使い方の長い説明が表示されます
systemd 機能も今のところはデフォルトで有効になっていないので設定が必要です
ディストリビューション内で 「/etc/wsl.conf」 に↓を追記します
ファイルがなければ作ります
WSL を再起動すると systemd が有効になっています
ただ注意しないといけないことがあって すでにインストール済みだったサービスの一部は起動に失敗していると思います
私の場合はたしか apache2 は動作していて postgresql や docker はダメでした
インストール時に systemd がないことでセットアップスクリプトが完了していないのだと思います
そういったパッケージは再インストールすることで正常に動作するようになりました
あと個人的に困ったところは ストア版にすると wsltty が起動しなくなりました
issue はすでにあって 軽く見た感じだと一応対処したリリースは出ていそうですが まだ issue がオープンになってるあたり完全ではないのかもです
https://github.com/mintty/wsltty/issues/302
これを機に WSL も Windows Terminal に移してみようかと試していると意外と不便なところがありました
exit コマンドで終了するとき 引数なしだと最後のコマンドの exit コードを引き継ぐようです
例えば存在しないコマンドを実行したら 127 になっていて そのまま終了するとシェルが 127 のコードで終了します
Windows Terminal はタブのプロセスが正常以外のコードで終了するとタブを閉じてくれません
エラーがあるとわかるという点では良い機能と言えますが 毎回 exit に 0 を指定するのは面倒です
WSL1 / WSL2 とは別の区分で WSL の仕組み自体のバージョンがあるようで ストア版が 1.0 になったそうです
最初は特に興味もなく ストア版というと勝手にアップデートされて Windows Terminal みたいな問題起きそうとかそういうイメージでした
調べてみると ただインストールの方法が違うだけでなく追加機能が色々あるようです
また これまでの OS 組み込みの方は廃止はされないものの 積極的に開発されるものはストア版のようです
これまでの WSL は WSL1 みたいな扱いになるってことでしょうか
それなら気が向いたときに切り替えておこうかなくらいに考えてましたが systemd が使えるという情報を見つけました
WSL にしても Docker コンテナにしても systemd が使えないのはとても不便だったので これはすごく良さそうです
ということで早速切り替えました
やることは KB5020030 をインストールして 「wsl --update」 コマンドを実行するだけです
今のところはこのアップデートは自動インストールされないようで Windows Update の画面でオプションとして並んでいます
これを手動でインストールしてから 「wsl --update」 を実行します
これまで全然カーネルのアップデートをしていないと複数回の実行が必要になることもあるそうです
切り替えが終わると 「wsl --version」 コマンドでバージョンが見れるようになります
古いバージョンだと 「--version」 というオプションはないので無効なオプションというエラーメッセージと使い方の長い説明が表示されます
systemd 機能も今のところはデフォルトで有効になっていないので設定が必要です
ディストリビューション内で 「/etc/wsl.conf」 に↓を追記します
ファイルがなければ作ります
[boot]
systemd=true
WSL を再起動すると systemd が有効になっています
ただ注意しないといけないことがあって すでにインストール済みだったサービスの一部は起動に失敗していると思います
私の場合はたしか apache2 は動作していて postgresql や docker はダメでした
インストール時に systemd がないことでセットアップスクリプトが完了していないのだと思います
そういったパッケージは再インストールすることで正常に動作するようになりました
あと個人的に困ったところは ストア版にすると wsltty が起動しなくなりました
issue はすでにあって 軽く見た感じだと一応対処したリリースは出ていそうですが まだ issue がオープンになってるあたり完全ではないのかもです
https://github.com/mintty/wsltty/issues/302
これを機に WSL も Windows Terminal に移してみようかと試していると意外と不便なところがありました
exit コマンドで終了するとき 引数なしだと最後のコマンドの exit コードを引き継ぐようです
例えば存在しないコマンドを実行したら 127 になっていて そのまま終了するとシェルが 127 のコードで終了します
Windows Terminal はタブのプロセスが正常以外のコードで終了するとタブを閉じてくれません
エラーがあるとわかるという点では良い機能と言えますが 毎回 exit に 0 を指定するのは面倒です
これまでウェブサーバーなどのデーモン化するアプリケーションでログを出力するときは いつもロガーを自作して設定で指定したファイルに書き込んでました
ほぼ使いまわしですが微妙に面倒な部分でもあるんですよね
それに極稀にキャッチされないエラーがあって標準出力の方に流れていたり
最初から全部 console.log で標準出力に流せばいいのでは と思ったこともありましたが systemd のサービスとするときにうまくできませんでした
systemd のサービスの設定で StandardOutput というものがあり標準出力をどこに送るか設定できます
のようにすれば指定のファイルに書き込めます
これを使おうとしたのですが この機能に対応した systemd のバージョンは 236 です
そのとき使う OS は CentOS7 だったのですが CentOS7 の systemd は 219 です
systemd が古くて使えません
その後 CentOS8 になり systemd が 239 になりました
やっと使えると思ったのですが問題がありました
file: だと新規にファイルを作成します
リダイレクトで > を使うようなものです
再起動のたびにログがクリアされます
リダイレクトで > の代わりに >> を使うような感じで 追記モードで扱う必要があります
追記モードにするには
と file: を append: にします
しかし append: は systemd 240 からです
AlmaLinux8 でも CentOS8 の頃からバージョンは上がっていないので 239 です
1 足りないので使えません
一応 shell script を起動するサービスとして そのスクリプトが本来のプログラムを実行して標準出力を目的のファイルへリダイレクトするようにするという方法もあったのですが 余計なものを挟みたくなかったのでこの方法は使いませんでした
最近は AlmaLinux9 が出ていて AlmaLinux9 は 250 でした
やっと使えます
ということで今度なにか作るときには console.log で出力して systemd の設定ファイルでログファイルの場所を指定する感じにしようと思ってます
ほぼ使いまわしですが微妙に面倒な部分でもあるんですよね
それに極稀にキャッチされないエラーがあって標準出力の方に流れていたり
最初から全部 console.log で標準出力に流せばいいのでは と思ったこともありましたが systemd のサービスとするときにうまくできませんでした
systemd のサービスの設定で StandardOutput というものがあり標準出力をどこに送るか設定できます
StandardOutput=file:/path/to/log
のようにすれば指定のファイルに書き込めます
これを使おうとしたのですが この機能に対応した systemd のバージョンは 236 です
そのとき使う OS は CentOS7 だったのですが CentOS7 の systemd は 219 です
systemd が古くて使えません
その後 CentOS8 になり systemd が 239 になりました
やっと使えると思ったのですが問題がありました
file: だと新規にファイルを作成します
リダイレクトで > を使うようなものです
再起動のたびにログがクリアされます
リダイレクトで > の代わりに >> を使うような感じで 追記モードで扱う必要があります
追記モードにするには
StandardOutput=append:/path/to/log
と file: を append: にします
しかし append: は systemd 240 からです
AlmaLinux8 でも CentOS8 の頃からバージョンは上がっていないので 239 です
1 足りないので使えません
一応 shell script を起動するサービスとして そのスクリプトが本来のプログラムを実行して標準出力を目的のファイルへリダイレクトするようにするという方法もあったのですが 余計なものを挟みたくなかったのでこの方法は使いませんでした
最近は AlmaLinux9 が出ていて AlmaLinux9 は 250 でした
やっと使えます
ということで今度なにか作るときには console.log で出力して systemd の設定ファイルでログファイルの場所を指定する感じにしようと思ってます
state から計算できる値で毎回計算したくないのを useMemo でメモ化していました
あるとき重めの処理の中に async 関数が入り await が必要になりました
レンダリング処理の途中に await は書けないです
await しないと useMemo の結果は Promise です
こんなことを考えてみましたが React ではプロパティが途中で変わっても再レンダリングされなければ画面に影響しません
resolve されたタイミングで再レンダリングさせるには state を更新するしかないです
setState するなら useEffect の中なのでこんな感じになります
できれば避けたいやつです
毎回そういう事するのは避けたいのでフック化してこういう感じです
ただこれだけだと promise が変わったときにリセットできないのでやはり useEffect は必要です
また何度も then で関数を設定することになるので 1 回だけで済むように useEffect の中で行うようにします
引数の Promise は state に入れたり useMemo を使って同じ Promise を渡すようにしないと無限ループになります
だと then の中で setValue(1) が実行されて再レンダリングされるたびに Promise が新しくなり また useEffect 内で then がセットされてのループです
ちょっとした使い方のミスで無限ループするのは扱いづらいのかもしれないです
あるとき重めの処理の中に async 関数が入り await が必要になりました
レンダリング処理の途中に await は書けないです
await しないと useMemo の結果は Promise です
const promise = useMemo(() => {
return fn(foo, bar)
}, [fn, foo, bar])
こんなことを考えてみましたが React ではプロパティが途中で変わっても再レンダリングされなければ画面に影響しません
const value = useMemo(() => {
const result = {
value: null,
}
fn(foo, bar).then(result_value => {
result.value = result_value
})
return result
}, [fn, foo, bar])
resolve されたタイミングで再レンダリングさせるには state を更新するしかないです
setState するなら useEffect の中なのでこんな感じになります
useEffect(() => {
const promise = fn(foo, bar)
setState(null)
promise.then(result => {
setState(result)
})
}, [fn, foo, bar])
できれば避けたいやつです
毎回そういう事するのは避けたいのでフック化してこういう感じです
const usePromise = (promise) => {
const [value, setValue] = useState(null)
promise.then(result => setValue(result))
return value
}
const value = usePromise(promise)
ただこれだけだと promise が変わったときにリセットできないのでやはり useEffect は必要です
また何度も then で関数を設定することになるので 1 回だけで済むように useEffect の中で行うようにします
const usePromise = (promise) => {
const [value, setValue] = useState(null)
useEffect(() => {
setValue(null)
let alive = true
promise.then(result => {
alive && setValue(result)
})
return () => {
alive = false
}
}, [promise])
return value
}
const value = usePromise(promise)
引数の Promise は state に入れたり useMemo を使って同じ Promise を渡すようにしないと無限ループになります
const value = usePromise(Promise.resolve(1))
だと then の中で setValue(1) が実行されて再レンダリングされるたびに Promise が新しくなり また useEffect 内で then がセットされてのループです
ちょっとした使い方のミスで無限ループするのは扱いづらいのかもしれないです
state から作れる値は state に保存せずレンダリング時に毎回作るわけですが 重くなる処理は毎回作り直したくないので useMemo を使って毎回計算しないようにします
ただ その state を複数のコンポーネントに渡してそれらのコンポーネントでも同じ計算結果を使いたいことがあります
state とそれから作った値をまとめて props で渡すこともできますが 渡す値を増やしたくはないです
state があれば作れるのですから 本来渡すべきデータは state だけのはずです
でもそうすると state とそれから作る値を使う全部のコンポーネントで個別に useMemo で再計算なんですよね
なのでグローバルな useMemo が欲しいというわけです
グローバルならフックに頼れなくなりますが モジュール内に共通の保存する場所を作れば済みます
同じ state を指定したら同じ値になるとすると 同じ state から別の計算によって値を作るケースに対応できないので それを見分けるキーが必要になります
同じ state とキーならどのコンポーネントからでも同じ結果を返します
同じ計算なら関数も同じになるはずなので 関数をキーにします
これでいい感じに動きはするのですが data に古いデータがずっと残ってしまいます
保存を直近◯件に制限したり 保存したのが古くてもよく使うのは残すようにソートするなど工夫が必要そうです
ただ その state を複数のコンポーネントに渡してそれらのコンポーネントでも同じ計算結果を使いたいことがあります
state とそれから作った値をまとめて props で渡すこともできますが 渡す値を増やしたくはないです
state があれば作れるのですから 本来渡すべきデータは state だけのはずです
でもそうすると state とそれから作る値を使う全部のコンポーネントで個別に useMemo で再計算なんですよね
なのでグローバルな useMemo が欲しいというわけです
グローバルならフックに頼れなくなりますが モジュール内に共通の保存する場所を作れば済みます
同じ state を指定したら同じ値になるとすると 同じ state から別の計算によって値を作るケースに対応できないので それを見分けるキーが必要になります
同じ state とキーならどのコンポーネントからでも同じ結果を返します
同じ計算なら関数も同じになるはずなので 関数をキーにします
const data = []
const isSame = (a, b) =>
a.length === b.length &&
a.every((v, i) => v === b[i])
const globalMemo = (fn, deps) => {
const key = [fn, ...deps]
const item = data.find(x => isSame(x.key, key))
if (item) return item.value
const value = fn(...deps)
data.push({ key, value })
return value
}
これでいい感じに動きはするのですが data に古いデータがずっと残ってしまいます
保存を直近◯件に制限したり 保存したのが古くてもよく使うのは残すようにソートするなど工夫が必要そうです
finally も try で使うので try-catch と同じような書きづらさがあります
例えばこんな感じでスッキリとしません
どうにかしたいと思ってこういうものを作りました
fina 関数に関数を渡して実行します
引数で受け取る use 関数で finally の処理をセットします
リソースの確保と同時に解放時の処理を書けるので普通の try-finally よりスッキリとして見やすくなります
1 つめの引数に渡した値は use 自体の結果として受け取れます
また 2 つめの引数である finally で呼び出される関数の引数としても渡されます
同じスコープなら外側が見れるので単純に
というインターフェースでもよかったのですが use の引数として取得と解放がセットになるし 変数名を気にせず引数で受け取った名前で解放できるほうがいいかなと思ってこうしました
finally の用途は解放が必要なリソースに限らず 外部の変数の状態を書き換えて 抜ける場合はエラーでも必ず戻すというときにも使います
そういうときは 1 つめの引数は null などにして 2 つめの引数で戻す処理を書きます
1 つめの引数も finally の処理と対称になるように関数にすれば 外部の変数を書き換えたりする処理でも自然と use の引数にかけてまとまるとも思いましたが その場で呼び出すだけだし 関数にするほどでもないかなと思って非対称になってます
基本こういう処理はだいたい非同期処理が入るだろうと思って非同期前提の Promise を使った処理になっています
ただこういう同期処理(⇩例)の場合もあって 非同期化すると使う側にも伝播するので同期版もほしいなと思って同期・非同期両対応版も作りました
使う側はさっきと一緒です
例えばこんな感じでスッキリとしません
const fn = () => {
// なにか1
let foo
try {
foo = createFoo()
// なにか2
} finally {
destroyFoo(foo)
}
// なにか3
}
どうにかしたいと思ってこういうものを作りました
const fina = (fn) => {
const fins = []
const use = (value, fin) => {
fins.push(() => fin(value))
return value
}
return Promise.resolve(use).then(fn).finally(() => {
for (const fin of fins) fin()
})
}
fina(use => {
const foo = use(createFoo(), foo => destroyFoo(foo))
console.log(foo)
state.running = true
use(null, () => state.running = false)
})
fina 関数に関数を渡して実行します
引数で受け取る use 関数で finally の処理をセットします
リソースの確保と同時に解放時の処理を書けるので普通の try-finally よりスッキリとして見やすくなります
1 つめの引数に渡した値は use 自体の結果として受け取れます
また 2 つめの引数である finally で呼び出される関数の引数としても渡されます
同じスコープなら外側が見れるので単純に
fina(addFinally => {
const foo = createFoo()
addFinally(() => destroyFoo(foo))
console.log(foo)
})
というインターフェースでもよかったのですが use の引数として取得と解放がセットになるし 変数名を気にせず引数で受け取った名前で解放できるほうがいいかなと思ってこうしました
finally の用途は解放が必要なリソースに限らず 外部の変数の状態を書き換えて 抜ける場合はエラーでも必ず戻すというときにも使います
そういうときは 1 つめの引数は null などにして 2 つめの引数で戻す処理を書きます
1 つめの引数も finally の処理と対称になるように関数にすれば 外部の変数を書き換えたりする処理でも自然と use の引数にかけてまとまるとも思いましたが その場で呼び出すだけだし 関数にするほどでもないかなと思って非対称になってます
基本こういう処理はだいたい非同期処理が入るだろうと思って非同期前提の Promise を使った処理になっています
ただこういう同期処理(⇩例)の場合もあって 非同期化すると使う側にも伝播するので同期版もほしいなと思って同期・非同期両対応版も作りました
fina(use => {
const url = use(URL.createObjectURL(blob), url => URL.revokeObjectURL(url))
console.log(url)
})
const xsyncFinally = (fn, fin) => {
let handled = false
let ret
try {
const maybe_promise = fn()
if (maybe_promise instanceof Promise) {
ret = maybe_promise.finally(() => {
fin()
})
handled = true
} else {
ret = maybe_promise
}
return ret
} finally {
if (!handled) {
fin()
}
}
}
const fina = (fn) => {
const fins = []
const use = (value, fin) => {
fins.push(() => fin(value))
return value
}
return xsyncFinally(
() => fn(use),
() => { for (const fin of fins) fin() }
)
}
使う側はさっきと一緒です
ということがあったものの現状 JavaScript からユーザーが操作できるブラウザのページ表示倍率は触れないはず
○%以上のズームは許可しませんとかすると使う側からして不便になるし わからないでもない
transform でいいかと思ったけど これはレイアウト計算後にズームするから思い通りにならない
だけど 全体をズームするなら問題ない気がする と思って試してみたら思いの外問題なく動いた
ページサイズを 2 倍にする
流石にここまで大きいのはじゃまなので 125%
body に設定すると overflow が効かないので #root みたいな要素を作ってそこに設定
すべての要素は #root の中に配置する
Portal 的な機能で ダイアログ等を body 直下に置くとズーム対象外になるので注意
自作ページならどうにかなるけど ネット上にあるページを拡張機能とかで制御するのには向いてなさそう
拡張機能にするなら直接倍率制御できるかも?
○%以上のズームは許可しませんとかすると使う側からして不便になるし わからないでもない
transform でいいかと思ったけど これはレイアウト計算後にズームするから思い通りにならない
だけど 全体をズームするなら問題ない気がする と思って試してみたら思いの外問題なく動いた
ページサイズを 2 倍にする
#root {
width: 50vw;
height: 50vh;
overflow: auto;
transform: scale(2);
transform-origin: 0 0;
}
流石にここまで大きいのはじゃまなので 125%
#root {
width: 80vw;
height: 80vh;
overflow: auto;
transform: scale(1.25);
transform-origin: 0 0;
}
body に設定すると overflow が効かないので #root みたいな要素を作ってそこに設定
すべての要素は #root の中に配置する
Portal 的な機能で ダイアログ等を body 直下に置くとズーム対象外になるので注意
自作ページならどうにかなるけど ネット上にあるページを拡張機能とかで制御するのには向いてなさそう
拡張機能にするなら直接倍率制御できるかも?
最近ではブロックスコープに let/const でいいので即時関数呼び出しを使うことは減ってるものの if 文で return したいことがあるので未だに時々使います
よくあるカッコで
は最後を }()) とするか })() とするかで分かれたり () なので 2 文字かつ閉じカッコが必要なのが面倒です
さらに ( から始めるのはセミコロンなしスタイルと相性が悪いということで 単項演算子版を使ってます
! 以外でも単項演算子なら ~ や typeof を使っても動きます
! は書きやすい上に強調してる感があって 返り値を捨てる=副作用を起こすところ ということが目立っていいかなということでこれにしてました
最近 普段はめったにみない void 演算子を見て そういえばそんな演算子もあったなぁとか思っていたら ふと即時関数呼び出しに向いてそうと閃きました
文字数は増えましたが ! に比べて見た目が自然な感じです
! だと Boolean キャストして反転という処理でもありましたが void は何が来ようと undefined を返すだけです
返り値を捨てるという意味合いにぴったりで処理的にもムダを感じません
通常のコードで void を書くことはまずないので全体から検索するときにも ! より優れています
唯一の問題は文字数が増えることですが 打ちやすさ的には Shift+1 って結構押しづらくもあるので void のほうが早く打ててるかもしれません
知名度的には () の方法を除けば一番見るのが ! と言っても良いくらいには ! をみることはあるので ある程度 JavaScript を読んでる人には ! は伝わりそうですが void だと二度見されそうというのはありますね
よくあるカッコで
(function() {
//
})()
は最後を }()) とするか })() とするかで分かれたり () なので 2 文字かつ閉じカッコが必要なのが面倒です
さらに ( から始めるのはセミコロンなしスタイルと相性が悪いということで 単項演算子版を使ってます
!function() {
//
}()
! 以外でも単項演算子なら ~ や typeof を使っても動きます
! は書きやすい上に強調してる感があって 返り値を捨てる=副作用を起こすところ ということが目立っていいかなということでこれにしてました
最近 普段はめったにみない void 演算子を見て そういえばそんな演算子もあったなぁとか思っていたら ふと即時関数呼び出しに向いてそうと閃きました
void function () {
//
}()
文字数は増えましたが ! に比べて見た目が自然な感じです
! だと Boolean キャストして反転という処理でもありましたが void は何が来ようと undefined を返すだけです
返り値を捨てるという意味合いにぴったりで処理的にもムダを感じません
通常のコードで void を書くことはまずないので全体から検索するときにも ! より優れています
唯一の問題は文字数が増えることですが 打ちやすさ的には Shift+1 って結構押しづらくもあるので void のほうが早く打ててるかもしれません
知名度的には () の方法を除けば一番見るのが ! と言っても良いくらいには ! をみることはあるので ある程度 JavaScript を読んでる人には ! は伝わりそうですが void だと二度見されそうというのはありますね
これまではちょっとなにかしたいというときはとりあえず Node.js だった
だけど今でも Node.js は標準機能だけだと簡単なことでも不便なところがある
例えば http(s) リクエストとか
package.json を用意して色々インストールする前提だと困らないレベルだけど ちょっとしたスクリプトだけでパッケージのインストールが必要になるのはなんかイヤ
パッケージ内であっても メインの処理ではない極稀に使うことがある便利ツールくらいなもののために追加パッケージを入れるのも同じく避けたい
そういうのだとグローバルインストールがありかもしれないけど グローバルインストールはコマンド以外は避けたくもある
パッケージ側でインストール漏れてても動いてしまうせいで あとになって必要なパッケージの記載漏れに気づくとかあるし 初めて実行する環境だと事前準備でインストールが必要だし
Node.js にこだわる必要もないので他のツールを探すと PowerShell が良さそう
デフォルトで色々できるし Windows だと標準で使える
ちょっとしたスクリプトを動かしたい環境って基本は開発環境の Windows であって サーバ側で動かすようなものならちゃんとパッケージに含めた機能になるはず
PowerShell だとコマンド呼び出して結果のこの部分取り出して別のコマンドに渡して その結果によって次のコマンドを変えたり とかそういうのがすごくやりやすい
zx が出てきた頃は それに移行を考えてたけど Node.js な分 標準機能だと不足機能がある問題があるし .NET が Linux でも動くので PowerShell に揃えていくのもありかなって思った
だけど今でも Node.js は標準機能だけだと簡単なことでも不便なところがある
例えば http(s) リクエストとか
package.json を用意して色々インストールする前提だと困らないレベルだけど ちょっとしたスクリプトだけでパッケージのインストールが必要になるのはなんかイヤ
パッケージ内であっても メインの処理ではない極稀に使うことがある便利ツールくらいなもののために追加パッケージを入れるのも同じく避けたい
そういうのだとグローバルインストールがありかもしれないけど グローバルインストールはコマンド以外は避けたくもある
パッケージ側でインストール漏れてても動いてしまうせいで あとになって必要なパッケージの記載漏れに気づくとかあるし 初めて実行する環境だと事前準備でインストールが必要だし
Node.js にこだわる必要もないので他のツールを探すと PowerShell が良さそう
デフォルトで色々できるし Windows だと標準で使える
ちょっとしたスクリプトを動かしたい環境って基本は開発環境の Windows であって サーバ側で動かすようなものならちゃんとパッケージに含めた機能になるはず
PowerShell だとコマンド呼び出して結果のこの部分取り出して別のコマンドに渡して その結果によって次のコマンドを変えたり とかそういうのがすごくやりやすい
zx が出てきた頃は それに移行を考えてたけど Node.js な分 標準機能だと不足機能がある問題があるし .NET が Linux でも動くので PowerShell に揃えていくのもありかなって思った
React を使い始めてからずっと扱いづらいと思ってる部分の一つが state のリセット
コンポーネント内なら難しくないけど 外から受け取るデータに応じてのリセットがあまり考慮されてない気がする
そういう使い方を推奨してないんだろうけど やっぱりそういうことしたいときは結構出てくる
特に コンポーネント内の途中の状態を変更のたびに親に伝えるのは無駄だし 親の処理が増えてくる
ページのコンポーネントになってくると 子孫コンポーネントの状態更新の処理があれこれ混ざって複雑になってる
やっぱり理想はコンポーネントの中での操作が完了してから初めて親に伝えるもの
外から受け取ったデータを state に保持して 完了時に onXXX などで受け取った関数を呼び出すだけだからこれは普通にできる
問題なのは 2 回目以降
state は引き継がれるから前回の情報がコンポーネントに残ったまま
外から受け取るデータが変更されたタイミングで state をリセットしたいので こういう方法で対処してた
これの欠点は 変更時に 1 回ムダにレンダリング処理が行われること
それと useState が多いと setState みたいな関数を何度も呼び出さないといけないところ
コンポーネント内でリセット機能があってすでに関数を用意してるならそれを呼び出せばいいけど
あと useEffect のこういう使い方って正しいの?と思うところもある
その他の方法は key を使うこと
親コンポーネントが props を変えるときに key も変えればリセットできる
でもこれはコンポーネントを使う側に 追加でしてもらうことが増えてあまり良い方法じゃないと思う
key の変え忘れでバグになることが多そう
それで思ったのがコンポーネントを 2 重にして key の制御もコンポーネント側でやる方法
結構いい感じ
useMemo を使って Math.random() を返すことで依存プロパティが変わったときに key を変えることができる
変わったときにコンポーネントをリセットしたいプロパティを useMemo の依存プロパティに設定する
都度書くのも面倒なのでもう少し楽に作れるように
を用意しておいて 作るときは元にするコンポーネントと依存プロパティの名前の配列を引数で渡す
key を変える以上 コンポーネント全体を作り直すので リスナをつけていて一瞬でも解除したくないとか API の fetch をしていてリセットのタイミングで毎回 fetch したくないとかあるなら 外側コンポーネントを自作してリセットしたくないデータはそっちで準備して props で渡すか key を使わず state リセットする方法にしたほうが良さそう
コンポーネント内なら難しくないけど 外から受け取るデータに応じてのリセットがあまり考慮されてない気がする
そういう使い方を推奨してないんだろうけど やっぱりそういうことしたいときは結構出てくる
特に コンポーネント内の途中の状態を変更のたびに親に伝えるのは無駄だし 親の処理が増えてくる
ページのコンポーネントになってくると 子孫コンポーネントの状態更新の処理があれこれ混ざって複雑になってる
やっぱり理想はコンポーネントの中での操作が完了してから初めて親に伝えるもの
外から受け取ったデータを state に保持して 完了時に onXXX などで受け取った関数を呼び出すだけだからこれは普通にできる
問題なのは 2 回目以降
state は引き継がれるから前回の情報がコンポーネントに残ったまま
外から受け取るデータが変更されたタイミングで state をリセットしたいので こういう方法で対処してた
const Component = ({ value }) => {
const [state, setState] = useState(value)
useEffect(() => {
setState(value)
}, [value])
return (
...
)
}
これの欠点は 変更時に 1 回ムダにレンダリング処理が行われること
それと useState が多いと setState みたいな関数を何度も呼び出さないといけないところ
コンポーネント内でリセット機能があってすでに関数を用意してるならそれを呼び出せばいいけど
あと useEffect のこういう使い方って正しいの?と思うところもある
その他の方法は key を使うこと
親コンポーネントが props を変えるときに key も変えればリセットできる
でもこれはコンポーネントを使う側に 追加でしてもらうことが増えてあまり良い方法じゃないと思う
key の変え忘れでバグになることが多そう
それで思ったのがコンポーネントを 2 重にして key の制御もコンポーネント側でやる方法
const ComponentInternal = ({ value }) => {
const [state, setState] = useState(value)
return (
...
)
}
const Component = (props) => {
const key = useMemo(() => Math.random(), [props.value])
return <ComponentInternal key={key} {...props} />
}
結構いい感じ
useMemo を使って Math.random() を返すことで依存プロパティが変わったときに key を変えることができる
変わったときにコンポーネントをリセットしたいプロパティを useMemo の依存プロパティに設定する
都度書くのも面倒なのでもう少し楽に作れるように
const resetStateComponent = (Component, deps) => (props) => {
const key = useMemo(() => Math.random(), deps.map(d => props[d]))
return <Component key={key} {...props} />
}
を用意しておいて 作るときは元にするコンポーネントと依存プロパティの名前の配列を引数で渡す
const Component = resetStateComponent(ComponentInternal, ["value"])
key を変える以上 コンポーネント全体を作り直すので リスナをつけていて一瞬でも解除したくないとか API の fetch をしていてリセットのタイミングで毎回 fetch したくないとかあるなら 外側コンポーネントを自作してリセットしたくないデータはそっちで準備して props で渡すか key を使わず state リセットする方法にしたほうが良さそう
React で input イベントごとにレンダリングしてると 入力が遅く感じる時がある
大きめの配列でそれぞれの処理も複雑なときにありがち
単純に要素ごとに 100ms 待機させた例
これで文字を打つと遅く感じる
items が変わらなければ ul の中はずっと同じなんだから useMemo でメモしておけばいい
理想的な書き方は ul の中に直接書いてこういう感じ
この場合はこれでも動くけどフックは関数の最初にまとめておきたい
フックは呼び出し順の違いや呼び出されない場合があったらダメなど特殊なルールがあるので 普通の関数みたいに自由に使うと思わぬ問題が出てきそうで それのために使うたびにここは大丈夫かみたいなことを慎重に確認とかしたくないし
でもそうなると ul の中だけが先に来てコンポーネント定義が読みづらくなる
先にフックの use だけ書いておいて JSX 中にメモしたい内容をかけるようなフックを useRef を使って自作
見やすくかけるようになった
大きめの配列でそれぞれの処理も複雑なときにありがち
単純に要素ごとに 100ms 待機させた例
const Component = ({ items }) => {
const [text, setText] = useState("")
const onChange = (event) => setText(event.target.value)
return (
<div>
<input value={text} onChange={onChange} />
<p>{text}</p>
<ul>
{items.map((item, index) => {
const d = Date.now()
while (Date.now() < d + 100) { }
return (
<li key={index}>{item.name}</li>
)
})}
</ul>
</div>
)
}
これで文字を打つと遅く感じる
items が変わらなければ ul の中はずっと同じなんだから useMemo でメモしておけばいい
理想的な書き方は ul の中に直接書いてこういう感じ
const Component = ({ items }) => {
const [text, setText] = useState("")
const onChange = (event) => setText(event.target.value)
return (
<div>
<input value={text} onChange={onChange} />
<p>{text}</p>
<ul>
{useMemo(() => {
return items.map((item, index) => {
const d = Date.now()
while (Date.now() < d + 100) { }
return (
<li key={index}>{item.name}</li>
)
})
}, [items])}
</ul>
</div>
)
}
この場合はこれでも動くけどフックは関数の最初にまとめておきたい
フックは呼び出し順の違いや呼び出されない場合があったらダメなど特殊なルールがあるので 普通の関数みたいに自由に使うと思わぬ問題が出てきそうで それのために使うたびにここは大丈夫かみたいなことを慎重に確認とかしたくないし
でもそうなると ul の中だけが先に来てコンポーネント定義が読みづらくなる
先にフックの use だけ書いておいて JSX 中にメモしたい内容をかけるようなフックを useRef を使って自作
const useCache = () => {
const ref = useRef()
return (...a) => {
const fn = a.pop()
if (ref.current &&
ref.current.deps.length === a.length &&
ref.current.deps.every((e, i) => e === a[i])
) {
return ref.current.cache
}
const next = fn(...a)
ref.current = { deps: a, cache: next }
return next
}
}
const Component = ({ items }) => {
const [text, setText] = useState("")
const onChange = (event) => setText(event.target.value)
const cache = useCache()
return (
<div>
<input value={text} onChange={onChange} />
<p>{text}</p>
<ul>
{cache(items, () => {
return items.map((item, index) => {
const d = Date.now()
while (Date.now() < d + 100) { }
return (
<li key={index}>{item.name}</li>
)
})
})}
</ul>
</div>
)
}
見やすくかけるようになった
React の state ってイミュータブルが前提になってます
深い部分のプロパティを変えるのが面倒です
state.foo.bar.baz[1] をインクリメントするだけでこうです
オブジェクトのプロパティを書き換えたらその情報をもとにイミュータブルなまま新しいオブジェクトを作ってくれるライブラリもありますが これだけのためにライブラリ入れるのもなという気持ちがあります
単純にこういうフックを使えば良い気がしました
useState の setState 関数は引数として受け取る値が同じならレンダリングをスキップします
オブジェクトのプロパティ変更だと同じオブジェクト扱いでレンダリングされません
それを回避するために setState に渡すオブジェクトの参照だけを毎回変えています
setState の不要なレンダリングを避けてくれるメリットを捨てていますが 普段の setState でも毎回新しいオブジェクトを渡していて毎回レンダリングされます
それならこれでもいいと思うんです
このフックの使い方はこういう感じで プロパティを直接変更します
少し使っただけでは問題なさそうだったのですが 実際の使用になると問題がありました
state.foo をコンポーネントの props やフックの依存配列に渡すときです
foo の中に変更があったのに foo が同じオブジェクトなので変更を検知できません
複雑なオブジェクトを state に持つ場合って部分的なプロパティを props や依存配列に渡すことが多いので 実用はできなさそうです
やっぱり面倒なコードを書くかライブラリだよりになりそうです
深い部分のプロパティを変えるのが面倒です
state.foo.bar.baz[1] をインクリメントするだけでこうです
setState({
...state,
foo: {
...state.foo,
bar: {
...state.foo.bar,
baz: state.foo.bar.baz.map((x, i) => i === 1 ? x + 1 : x)
}
}
})
オブジェクトのプロパティを書き換えたらその情報をもとにイミュータブルなまま新しいオブジェクトを作ってくれるライブラリもありますが これだけのためにライブラリ入れるのもなという気持ちがあります
単純にこういうフックを使えば良い気がしました
const useMutableState = (init) => {
const [state, setState] = useState({ value: init })
const updateState = useCallback((update) => {
update(state.value)
setState({ value: state.value })
})
return [state.value, updateState]
}
useState の setState 関数は引数として受け取る値が同じならレンダリングをスキップします
オブジェクトのプロパティ変更だと同じオブジェクト扱いでレンダリングされません
それを回避するために setState に渡すオブジェクトの参照だけを毎回変えています
setState の不要なレンダリングを避けてくれるメリットを捨てていますが 普段の setState でも毎回新しいオブジェクトを渡していて毎回レンダリングされます
setState({ ...state, [prop]: value })
それならこれでもいいと思うんです
このフックの使い方はこういう感じで プロパティを直接変更します
const Component = () => {
const [state, updateState] = useMutableState(
{foo: {bar: {baz: [0, 0]}}}
)
const onClick = () => {
updateState(s => {
s.foo.bar.baz[1]++
})
}
return (
<button onClick={onClick}>{state.foo.bar.baz[1]}</button>
)
}
少し使っただけでは問題なさそうだったのですが 実際の使用になると問題がありました
state.foo をコンポーネントの props やフックの依存配列に渡すときです
foo の中に変更があったのに foo が同じオブジェクトなので変更を検知できません
複雑なオブジェクトを state に持つ場合って部分的なプロパティを props や依存配列に渡すことが多いので 実用はできなさそうです
やっぱり面倒なコードを書くかライブラリだよりになりそうです
ページ上で JavaScript コードを書いて実行できる環境で window や location などにアクセスできなくしたいとき
window などは上書きできないのでローカルスコープに同名変数を作って そのスコープでコードを実行する
document などから辿れるのでそれらも同じように隠す
これだと this から window を参照できる
use strict をつけて this が window にならないようにする
これでも Function 関数を使えばグローバルスコープの関数を作れるので location などにアクセスできる
単純に Function を null にしても リテラルから関数を作れる
Function.prototype.constructor も隠す
ジェネレータや async 関数も同じようにする
Worker のスコープでの実行も避けるので Worker も隠す
思いつく方法は使えなくできたけど これで完全?
window などは上書きできないのでローカルスコープに同名変数を作って そのスコープでコードを実行する
const exec = (source) => {
const window = {}
const location = null
const self = null
eval(source)
}
exec(`
console.log(location)
console.log(window.location)
`)
// null
// undefined
document などから辿れるのでそれらも同じように隠す
これだと this から window を参照できる
exec(`
console.log((function() { return this })().location)
`)
use strict をつけて this が window にならないようにする
const exec = (source) => {
"use strict"
const window = {}
const location = null
const self = null
eval(source)
}
これでも Function 関数を使えばグローバルスコープの関数を作れるので location などにアクセスできる
exec(`
Function("console.log(location)")()
`)
単純に Function を null にしても リテラルから関数を作れる
const exec = (source) => {
"use strict"
const window = {}
const location = null
const self = null
const Function = null
eval(source)
}
exec(`
(function() {}).constructor("console.log(location)")()
`)
Function.prototype.constructor も隠す
Function.prototype.constructor = null
const exec = (source) => {
"use strict"
const window = {}
const location = null
const self = null
const Function = null
eval(source)
}
ジェネレータや async 関数も同じようにする
Worker のスコープでの実行も避けるので Worker も隠す
思いつく方法は使えなくできたけど これで完全?
const handler = (ctx, name) => (...args) => ctx.prototype[name].call(...args)
const S = ctor => new Proxy(ctor, { get: handler })
使用例
S(Array).reverse([1, 2, 3])
// [3, 2, 1]
S(String).slice("foobar", 2, 4)
// ob
パイプライン演算子が来たりでもないと基本はメソッドで良さそう
数少ない役立つところは map などの関数に入れる使い方
const SString = S(String)
const texts = [" a ", " b"].map(SString.trim)
// ["a", "b"]
毎回 「x => x.trim()」 って書くの疲れるし
文字数的にはそんなにだけど記号打つのが面倒
この方法だと prototype のメソッドを call するのと一緒なのでキャストされる
配列風なものを配列として扱いたいとかでは使えるけど 変換いらずにただその名前のメソッドを呼び出せればいいならこれで十分そう
const handler = (ctx, name) => (value, ...args) => value[name](...args)
const X = new Proxy({}, { get: handler })
const texts = [" a ", " b"].map(X.trim)
// ["a", "b"]
X.trim が引数の trim を呼び出す関数になるなら
parseInt(X, 2) で X のところに引数が入って呼び出される関数にもなってほしい
parseInt そのままではただの関数呼び出しに X が渡されるだけなので一工夫する
const handler = (ctx, name) => (value, ...args) => value[name](...args)
const fn = f => (...args) => a => f(...args.map(x => x === X ? a : x))
const X = new Proxy(fn, { get: handler })
X に関数を渡して その返り値を関数として使う
const hex2dec = X(parseInt)(X, 16)
hex2dec("ff")
// 255
const bit = X(Math.pow)(2, X)
bit(16)
// 65536
見やすさはなんかいまいち
やっぱり構文として対応してほしい
けど PHP では提案があったけど複雑すぎるとかで却下された話を聞いたし JavaScript でも入らなそうな気はする
少し長くなるけどアロー関数でできるといえばできるし 関数本体部分でちゃんと呼び出し形式になっていてどう実行されるかがわかりやすいし
X => parseInt(X, 2)
X => X.trim()
ページのロード時に全部の Custom Element の定義を読み込むのはムダが多い
めったに開かないページでしか使わないものまでロードする必要ないし
基本は Custom Element の定義ごとにファイルを分けてモジュール化してるので モジュールごとに内部で使う Custom Element のモジュールを import するのが一般的だと思う
この方法でも よく使うモジュールを毎回 import したり Shadow DOM 内の HTML を変えるたびに import も見直しが必要で面倒
未定義要素が Document や ShadowRoot に接続されたときにイベントが起きれば そのイベントのリスナでモジュールを読み込んで customElements.define を呼び出しできるのに
PHP の autoload 風で良さそう
ただ 今のところはそんなイベントは無いみたい
自分でやるなら MutationObserver 使って全部の Node の追加を監視して 「-」 を含むタグ名の要素があれば定義済みかチェックすることになりそう
DOM の更新が多いページは少し重そうなのと Shadow DOM 内を検知できないのが問題点
この機能用に HTMLElement を継承したクラスを作って 全部の Custom Element がこのクラスを継承するように作れば 共通部分で MutationObserver の監視設定を入れて Shadow DOM 内も要素の追加を監視できるけど ライブラリを使うとか既存のコンポーネントを使うとかもあってその制限は結構不便
attachShadow を上書きしてしまうのが簡単
とりあえずこんなので動きそう
import するところは仮の console.log にしてる
実際のロード処理は
みたいに 1 ファイルを動的 import して import するモジュール内で customElements.define まで行う
Custom Element を定義するモジュールがすべて同じフォルダにない場合は importmaps でそれぞれの場所を設定で良いかも
めったに開かないページでしか使わないものまでロードする必要ないし
基本は Custom Element の定義ごとにファイルを分けてモジュール化してるので モジュールごとに内部で使う Custom Element のモジュールを import するのが一般的だと思う
この方法でも よく使うモジュールを毎回 import したり Shadow DOM 内の HTML を変えるたびに import も見直しが必要で面倒
未定義要素が Document や ShadowRoot に接続されたときにイベントが起きれば そのイベントのリスナでモジュールを読み込んで customElements.define を呼び出しできるのに
PHP の autoload 風で良さそう
ただ 今のところはそんなイベントは無いみたい
自分でやるなら MutationObserver 使って全部の Node の追加を監視して 「-」 を含むタグ名の要素があれば定義済みかチェックすることになりそう
DOM の更新が多いページは少し重そうなのと Shadow DOM 内を検知できないのが問題点
この機能用に HTMLElement を継承したクラスを作って 全部の Custom Element がこのクラスを継承するように作れば 共通部分で MutationObserver の監視設定を入れて Shadow DOM 内も要素の追加を監視できるけど ライブラリを使うとか既存のコンポーネントを使うとかもあってその制限は結構不便
attachShadow を上書きしてしまうのが簡単
とりあえずこんなので動きそう
const mo = new MutationObserver((mutations) => {
const added_nodes = mutations.flatMap((m) => [...m.addedNodes])
const collect = (node) => {
for (const child of node.childNodes) {
added_nodes.push(child)
collect(child)
}
}
added_nodes.slice().forEach(collect)
const undefined_tags = added_nodes
.map((n) => n.localName)
.filter((t) => t && t.includes("-") && !customElements.get(t))
for (const tag of new Set(undefined_tags)) {
// load module and define custom element
// (ex) import(`/path/to/components/${tag}.js`)
console.log(tag)
}
})
const mo_conf = { childList: true, subtree: true }
mo.observe(document.body, mo_conf)
const attachShadow = Element.prototype.attachShadow
Element.prototype.attachShadow = function (...args) {
const shadow = attachShadow.apply(this, args)
mo.observe(shadow, mo_conf)
return shadow
}
import するところは仮の console.log にしてる
実際のロード処理は
import(`/components/${tag}.js`)
みたいに 1 ファイルを動的 import して import するモジュール内で customElements.define まで行う
Custom Element を定義するモジュールがすべて同じフォルダにない場合は importmaps でそれぞれの場所を設定で良いかも
https じゃないと動かない機能をローカルで試したいけど 自作や適当なサーバからダウンロードした証明書だとエラーが出る
毎回無視するボタンを押せばいいんだけどこれも面倒だからエラーが出ないようにしたい
ちゃんとやるなら ドメインに合わせた証明書を自作して信頼されたルート証明機関のところにインポートすればいいんだけどこれが結構面倒
適当なサイトの証明書をダウンロードしてローカルのサーバに設置して hosts を編集してそのサーバへのアクセスを VM とか localhost に向ければ済むような……と思ったけどサーバ側には証明書以外に秘密鍵も必要で秘密鍵はさすがに公開されてないので この方法は無理そう
実在サーバの証明書を使うという方向で考えてみると devtools のローカルオーバーライドが良さそう
以前実装されたときに良さそうと思った割に結局使わず忘れてた
使い方は devtools を開いて Source タブを開く
左側のサイドバーのタブで Overrides を開いて + ボタンを押す
フォルダ選択画面が出るので適当なところにフォルダを作って選択
devtools からそのフォルダへフルアクセスされるって警告が出るので許可
あとはそのフォルダ内にファイルを作れば devtools が開いてるときは本来のレスポンスの代わりに配置したファイルがレスポンスになる
例としてこのブログを使うと 「https://let.blog.jp/foo.html」 は 404 エラーになる
許可したフォルダの中に 「let.blog.jp/foo.html」 というファイルを配置する
配置できたら foo.html の URL にアクセスすると配置した HTML ファイルが開かれ 20 というアラートが表示されるようになる
JavaScript で処理してるハッシュ値計算は https じゃないと使えない機能
これくらいなら適当な https ページで devtools 開いてコンソールにコピペでいいんだけど ちょっと長くなって HTML や JavaScript ファイルを開いて動作確認したいってときには便利
注意するなら localStorage などのデータは本来のサーバのエリアに保存される
オーバーライドしてないページを開いたときの処理で localStorage のデータをサーバに送信するとかあれば ローカル感覚で保存した情報が送信されることはあり得る
あと 本来のページが見えなくなったりもするけど 使うことなさそうなサイトの URL を使ったり そもそも devtools 開かなければいいのでこっちは問題なさそう
この方法だとサーバがいらずにブラウザのみで良くなったという利点はあるけど サーバ側の処理もしたい場合には使えないのが欠点
毎回無視するボタンを押せばいいんだけどこれも面倒だからエラーが出ないようにしたい
ちゃんとやるなら ドメインに合わせた証明書を自作して信頼されたルート証明機関のところにインポートすればいいんだけどこれが結構面倒
適当なサイトの証明書をダウンロードしてローカルのサーバに設置して hosts を編集してそのサーバへのアクセスを VM とか localhost に向ければ済むような……と思ったけどサーバ側には証明書以外に秘密鍵も必要で秘密鍵はさすがに公開されてないので この方法は無理そう
実在サーバの証明書を使うという方向で考えてみると devtools のローカルオーバーライドが良さそう
以前実装されたときに良さそうと思った割に結局使わず忘れてた
使い方は devtools を開いて Source タブを開く
左側のサイドバーのタブで Overrides を開いて + ボタンを押す
フォルダ選択画面が出るので適当なところにフォルダを作って選択
devtools からそのフォルダへフルアクセスされるって警告が出るので許可
あとはそのフォルダ内にファイルを作れば devtools が開いてるときは本来のレスポンスの代わりに配置したファイルがレスポンスになる
例としてこのブログを使うと 「https://let.blog.jp/foo.html」 は 404 エラーになる
許可したフォルダの中に 「let.blog.jp/foo.html」 というファイルを配置する
<script>
crypto.subtle.digest("SHA-1", new ArrayBuffer()).then(buf => {
alert(buf.byteLength) // 20
})
</script>
配置できたら foo.html の URL にアクセスすると配置した HTML ファイルが開かれ 20 というアラートが表示されるようになる
JavaScript で処理してるハッシュ値計算は https じゃないと使えない機能
これくらいなら適当な https ページで devtools 開いてコンソールにコピペでいいんだけど ちょっと長くなって HTML や JavaScript ファイルを開いて動作確認したいってときには便利
注意するなら localStorage などのデータは本来のサーバのエリアに保存される
オーバーライドしてないページを開いたときの処理で localStorage のデータをサーバに送信するとかあれば ローカル感覚で保存した情報が送信されることはあり得る
あと 本来のページが見えなくなったりもするけど 使うことなさそうなサイトの URL を使ったり そもそも devtools 開かなければいいのでこっちは問題なさそう
この方法だとサーバがいらずにブラウザのみで良くなったという利点はあるけど サーバ側の処理もしたい場合には使えないのが欠点
something(`.... ... ${values.foo} .. ...`)
something(`.. ..... ${values.bar} ... ..`)
こんな感じの処理で values.foo などの埋め込む値が 存在する (null, undefined じゃない) ときのみ something を実行したいことがたまにある
if (values.foo) {
something(`.... ... ${values.foo} .. ...`)
}
if (values.bar) {
something(`.. ..... ${values.bar} ... ..`)
}
なんか面倒な感じで something 呼び出しが 10 近くも並ぶと どうにかしたくなってくる
`` の埋め込みはタグ関数を使って関数で処理できるので
const F = (fn, ...args) => (tpls, ...values) => {
if (values.every(v => v != null)) {
fn(String.raw(tpls, ...values), ...args)
}
}
F(something)`.... ... ${values.foo} .. ...`
F(something)`.. ..... ${values.bar} ... ..`
スッキリはしたけど用途が限定的すぎる気も
something(fn`... .. ${values.foo} .. ....`)
上の形式にして fn では埋め込む値に undefined があれば全体を undefined で返す
something では undefined を受け取ると何もしないとする方が良さそう?
ただ something が自作でないなら undefined 対策にラッパー関数が必要になるし ラッパー関数作るならそこで分岐埋め込みもやったほうがいいのではという気持ちにもなる
const somethingWrapper = (cond, str) => {
if (cond != null) something(str)
}
somethingWrapper(values.foo, `... .. ${values.foo} .. ..`)
シンプルだけど values.foo を 2 回書くのだけが不満
`` を使った埋め込みで 2 回書かないならタグ関数必須のはずだし 結局上のに戻る
文字列のパースで 無いかもしれないものがいくつかある場合 正規表現で「?」をつけて match させてから結果を if 文で色々処理するのってわかりづらいし let 多用になるのがいまいち
「?」つけずにそれぞれのパターンで match させてパターンごとに処理をさせたほうがわかりやすいかも
こういう関数を用意して
こう使う
見やすさはイマイチかも
「?」つけずにそれぞれのパターンで match させてパターンごとに処理をさせたほうがわかりやすいかも
こういう関数を用意して
const match = (str, patterns) => {
for(const [cond, fn] of patterns) {
if (cond == null) return fn()
const matched = cond instanceof RegExp ? cond.exec(str) : str.match(cond)
if (matched) return fn(matched, ...matched)
}
}
こう使う
const tes = str => {
const result = match(str, [
[/^(\d+)$/, (_, __, d) => [+d, +d]],
[/^(\d+),$/, (_, __, d) => [+d, null]],
[/^,(\d+)$/, (_, __, d) => [null, +d]],
[/^(\d+),(\d+)$/, (_, __, d1, d2) => [+d1, +d2]],
[null, () => [null, null]],
])
console.log(result)
}
tes("10,20") // [10, 20]
tes("30,") // [30, null]
tes(",40") // [null, 40]
tes("50") // [50, 50]
tes("") // [null, null]
見やすさはイマイチかも
前回の続き
前は JavaScript だけ扱いを特別にしたけど HTML ファイル中にあったほうがいいかなと思ったので HTML ファイルに全部書く
使う側は一緒で foo-bar.js を読み込んで foo-bar タグを書いておく
[page.html]
[foo-bar.js]
コンポーネントを import して customElements.define で定義
[base.js]
importComponent は HTML を fetch してパースしていろいろ
script を含んだコンポーネントの HTML
[foo-bar.html]
script タグ中のエクスポートは Node.js 形式で module.exports に代入
Base を継承したクラスをエクスポートする
前は JavaScript だけ扱いを特別にしたけど HTML ファイル中にあったほうがいいかなと思ったので HTML ファイルに全部書く
使う側は一緒で foo-bar.js を読み込んで foo-bar タグを書いておく
[page.html]
<script type="module" src="foo-bar.js"></script>
<foo-bar></foo-bar>
[foo-bar.js]
import { importComponent } from "./base.js"
importComponent("foo-bar.html").then(component => {
customElements.define("foo-bar", component)
})
コンポーネントを import して customElements.define で定義
[base.js]
export const importComponent = async (htmlpath) => {
const res = await fetch(htmlpath)
const html = await res.text()
const doc = new DOMParser().parseFromString(html, "text/html")
const fragment = document.createDocumentFragment()
const scripts = [...doc.querySelectorAll("script")]
scripts.forEach(x => x.remove())
fragment.append(...doc.head.childNodes, ...doc.body.childNodes)
const module = {}
class Base extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" })
this.shadowRoot.append(this.constructor.fragment.cloneNode(true))
this.elements = Object.fromEntries(
Array.from(
this.shadowRoot.querySelectorAll("[id]"),
elem => [elem.id, elem]
)
)
}
}
Base.fragment = fragment
for(const script of scripts) {
const source = script.textContent
const fn = Function("module", "Base", source)
fn(module, Base)
}
return module.exports
}
importComponent は HTML を fetch してパースしていろいろ
script を含んだコンポーネントの HTML
[foo-bar.html]
<style>
.foo { font-size: 20px; }
.bar { color: red; }
.active { font-weight: bold; }
</style>
<div>
<div class="foo">FOO</div>
<div class="bar">BAR</div>
<button id="btn">Button</button>
</div>
<script>
module.exports = class extends Base {
connectedCallback() {
this.elements.btn.addEventListener("click", (eve) => {
eve.target.classList.toggle("active")
})
}
}
</script>
script タグ中のエクスポートは Node.js 形式で module.exports に代入
Base を継承したクラスをエクスポートする
ふと HTMLImports を思い出したのでこんなことしてみた
[base.js]
[foo-bar.js]
[foo-bar.html]
innerHTML やスタイルは別の HTML ファイルに書く
script タグは HTML に含めず import される JavaScript ファイル側に書いてる
CustomElement の constructor の処理で HTML を fetch して ShadowDOM に append
表示するためのページ
[page.html]
[base.js]
const initCustomElement = async (template) => {
const res = await fetch(template)
const html = await res.text()
const doc = new DOMParser().parseFromString(html, "text/html")
const fragment = document.createDocumentFragment()
fragment.append(...doc.head.childNodes, ...doc.body.childNodes)
return fragment
}
export class Base extends HTMLElement {
constructor() {
super()
const C = this.constructor
if (!C.ready) {
C.ready = initCustomElement(C.template)
}
this.attachShadow({ mode: "open" })
C.ready.then((fragment) => {
this.shadowRoot.append(fragment.cloneNode(true))
})
}
}
[foo-bar.js]
import { Base } from "./base.js"
customElements.define("foo-bar", class extends Base {
static template = "foo-bar.html"
})
[foo-bar.html]
<style>
.foo { font-size: 20px; }
.bar { color: red; }
</style>
<div>
<div class="foo">FOO</div>
<div class="bar">BAR</div>
</div>
innerHTML やスタイルは別の HTML ファイルに書く
script タグは HTML に含めず import される JavaScript ファイル側に書いてる
CustomElement の constructor の処理で HTML を fetch して ShadowDOM に append
表示するためのページ
[page.html]
<script type="module" src="foo-bar.js"></script>
<foo-bar></foo-bar>
HTML をエスケープをしてもユーザ入力を a タグの href に入れることがあれば 「javascript:alert(1)」 みたいに javascript: の URL を書いて XSS できるとかいうのを見かけた
だけど 今どき javascript: なんて使わないし 無効化してしまえばいい気がする
毎回これ書くのも面倒だし もう少し楽に無効化できればいいんだけど……
もういっそデフォルト無効化でもいいのに互換性優先だからそれはなさそうだし
だけど 今どき javascript: なんて使わないし 無効化してしまえばいい気がする
window.addEventListener("click", eve => {
const a = eve.target.closest("a")
if (a && a.href.startsWith("javascript:")) {
eve.preventDefault()
}
})
毎回これ書くのも面倒だし もう少し楽に無効化できればいいんだけど……
もういっそデフォルト無効化でもいいのに互換性優先だからそれはなさそうだし
onxxx だとプロパティとして保持して 1 つだけ
on() や addEventListener(...) だと配列などで保持して複数
というのが一般的
だけど on で登録する関数が前に登録されてた関数を受け取れるようにすれば 保持する関数はひとつでいいし 呼び出し順の制御やそもそも呼び出さないとかできて便利かも?
on() や addEventListener(...) だと配列などで保持して複数
というのが一般的
だけど on で登録する関数が前に登録されてた関数を受け取れるようにすれば 保持する関数はひとつでいいし 呼び出し順の制御やそもそも呼び出さないとかできて便利かも?
let fn = () => {}
const on = (new_fn) => {
fn = new_fn.bind(null, fn)
}
on(() => {
console.log(1)
})
on(old => {
old()
console.log(2)
})
on(old => {
console.log(3)
old()
})
fn()
// 3
// 1
// 2
ライブドアブログが https 対応したっぽいのでこっちのサブブログで試してみてます
こっちは余計なものは極力入れない作りにしてるので http のリソースのロードも特になくて問題なさそうです
個人的にブログみたいなものに https は不要だと思うのでメインの方は特に変える予定はないです
JavaScript の機能で http だと制限されるものもありますが そういう機能を使うツールなどはすでに Gist に置くようにしているので今更変えるの面倒ですし
ところで 証明書を見てみたところ Let's Encrypt でした
無料ですし 最近ならまぁそれですよね
ただ Let's Encrypt だと Android 7.1 以降は対応しないみたいな情報もあって 離れる人も多そうな気はします
当初の予定だと今月末の 9/29 で見れなくなるそうですが ルート証明書を古い方で作っていれば来年の 9/29 までは見れるそうです
ライブドアブログの証明書のルート証明書は古い方の DST Root CA X3 だったので来年までは見れそうです
今じゃ Android 11 が出ようとしてるのに 7.1 なんてと思いますが Android 端末はメーカーが更新を出さないので上げられないんですよね
少し前に買った Android タブレットは結局一度もアップデートが来ないで 6 か 7 でした
買った頃はすでに 8 か 9 が出ていたのに OS がリリースされても端末に搭載されるまで 1 年以上かかってたりです
ユーザの多いスマホならともかくその他の Android 搭載系端末じゃ基本アップデートはないものと考えていいくらいですし
1 年伸びたところで来年になってもまだ Android 7.1 以下のユーザはまだまだいそうですけどね
こっちは余計なものは極力入れない作りにしてるので http のリソースのロードも特になくて問題なさそうです
個人的にブログみたいなものに https は不要だと思うのでメインの方は特に変える予定はないです
JavaScript の機能で http だと制限されるものもありますが そういう機能を使うツールなどはすでに Gist に置くようにしているので今更変えるの面倒ですし
ところで 証明書を見てみたところ Let's Encrypt でした
無料ですし 最近ならまぁそれですよね
ただ Let's Encrypt だと Android 7.1 以降は対応しないみたいな情報もあって 離れる人も多そうな気はします
当初の予定だと今月末の 9/29 で見れなくなるそうですが ルート証明書を古い方で作っていれば来年の 9/29 までは見れるそうです
ライブドアブログの証明書のルート証明書は古い方の DST Root CA X3 だったので来年までは見れそうです
今じゃ Android 11 が出ようとしてるのに 7.1 なんてと思いますが Android 端末はメーカーが更新を出さないので上げられないんですよね
少し前に買った Android タブレットは結局一度もアップデートが来ないで 6 か 7 でした
買った頃はすでに 8 か 9 が出ていたのに OS がリリースされても端末に搭載されるまで 1 年以上かかってたりです
ユーザの多いスマホならともかくその他の Android 搭載系端末じゃ基本アップデートはないものと考えていいくらいですし
1 年伸びたところで来年になってもまだ Android 7.1 以下のユーザはまだまだいそうですけどね
JavaScript のクラス構文は static プロパティへのアクセスが面倒です
this.constructor.foo のような感じになります
this.constructor を使わなくてもクラス名でのアクセスはできます
ただ クラスごとに名前が違ってコピペすると修正しないといけません
さらにクラス名を変えたときにクラス内のコードまでチェックして修正が必要になります
それを避けるために常に同じ名前でクラスのコンストラクタを取得できる仕組みが欲しいです
それが this.constructor になります
でも長くて書くのが面倒なんですよね
それを楽にするためにこういうことしてみました
クラス名を使ってアクセスするのですがそれを常に $ にします
特殊な変数名ぽいので丁度いいと思います
ただクラス名が $ になってしまうと使うときに困るので エクスポートするときに適切な名前にします
モジュールに分けていると 1 つのモジュールファイルの中でクラスをいくつも定義してエクスポートすることはそうないと思いますし わりとありかなと思います
シンプルで使いやすいですし
this.constructor.foo のような感じになります
this.constructor を使わなくてもクラス名でのアクセスはできます
ただ クラスごとに名前が違ってコピペすると修正しないといけません
さらにクラス名を変えたときにクラス内のコードまでチェックして修正が必要になります
それを避けるために常に同じ名前でクラスのコンストラクタを取得できる仕組みが欲しいです
それが this.constructor になります
でも長くて書くのが面倒なんですよね
それを楽にするためにこういうことしてみました
const $ = class {
static foo = 1
bar() {
return $.foo + 1
}
}
export { $ as Class1 }import { Class1 } from "./module.js"
console.log(new Class1().bar())
// 2
クラス名を使ってアクセスするのですがそれを常に $ にします
特殊な変数名ぽいので丁度いいと思います
ただクラス名が $ になってしまうと使うときに困るので エクスポートするときに適切な名前にします
モジュールに分けていると 1 つのモジュールファイルの中でクラスをいくつも定義してエクスポートすることはそうないと思いますし わりとありかなと思います
シンプルで使いやすいですし
ふとローカルのスニペット管理ツール使ってよく使うのは選んでコピペするだけで済むようにしようと思いました
言語指定でちゃんとやるなら VSCode の機能があるけど もっとゆるくグローバルで使うもの
コード対応で軽量のメモツールってあまりないからとりあえず markdown 系メモの適当なやつ
それにこのブログの「コード」カテゴリのも登録しとこうかと思って自動で登録するためのところを作ったのでメモ
add.js はパースされた title と body を登録する処理
title は記事のタイトル
body はテキストで本文のコードブロックをマークダウン形式にしたもの
複数あるなら空行挟んで複数個のコードブロック
Node.js でもブラウザと同じように DOM 操作できる jsdom 便利
言語指定でちゃんとやるなら VSCode の機能があるけど もっとゆるくグローバルで使うもの
コード対応で軽量のメモツールってあまりないからとりあえず markdown 系メモの適当なやつ
それにこのブログの「コード」カテゴリのも登録しとこうかと思って自動で登録するためのところを作ったのでメモ
const fetch = require("node-fetch")
const { JSDOM } = require("jsdom")
const { add } = require("./add.js")
const main = async (url) => {
const res = await fetch(url)
const html = await res.text()
const dom = new JSDOM(html)
const articles = dom.window.document.querySelectorAll("main article")
for(const article of articles) {
const title = article.querySelector(".article-header .article-title").textContent
const body = Array.from(
article.querySelectorAll(".article-body pre code"),
code => {
for(const br of code.querySelectorAll("br")) {
br.replaceWith("\n")
}
const type = code.dataset.lang || ""
const c = code.textContent.trimEnd()
return "```" + type + "\n" + c + "\n```"
}
).join("\n\n")
add(title, body)
}
}
main("https://let.blog.jp/category/%E3%82%B3%E3%83%BC%E3%83%89")
yarn add node-fetch jsdom
node <script>.js
add.js はパースされた title と body を登録する処理
title は記事のタイトル
body はテキストで本文のコードブロックをマークダウン形式にしたもの
複数あるなら空行挟んで複数個のコードブロック
Node.js でもブラウザと同じように DOM 操作できる jsdom 便利
必要性はともかく変更不可版 Array ということでこれでよさそう
中身同じでも等しいものにならないのが不便かも
作ったタプルをすべて保存しておいて同じ中身なら既存のを返せばできそうだけどメモリ的につらそう
WeakSet を forEach できたらいいのに
中身同じでも等しいものにならないのが不便かも
class Tuple extends Array {
constructor(...a) {
super()
this.push(...a)
Object.freeze(this)
}
}
作ったタプルをすべて保存しておいて同じ中身なら既存のを返せばできそうだけどメモリ的につらそう
WeakSet を forEach できたらいいのに
SQL の列名は同じ種類の ID なら同じ名前にしておけば USING(xx_id) で JOIN できたり便利なので 列名を一意にしようなんて思ったことはありませんでした
しかし 「SELECT * FROM ~」 のように全取得してると LEFT JOIN で JOIN 先にマッチする行がないと id が null になってしまう問題に困りました
同じ列名なのでオブジェクトや連想配列やディクショナリとして取得するとあとの方の null で上書きされてしまうのですよね
「*」 を使わず全部書けば解決ですが 長いです
ワイルドカードで除外が選べれば助かるケースはかなりあるのですが SQL では対応してません
それに SQL って全然更新されないので JavaScript などのように待てば便利なる期待もありません
せめて 「select a.* as a__*」 で a__ プレフィックスつけてくれるみたいなのができたらよかったのですけど
それで思ったのが最初から列名を 「tablename__columnname」 みたいなのにしておくことです
見た目はいまいちですが JOIN が入ってくると列名が被る可能性があって 結局 WHERE や SELECT で 「tablename.columnname」 のようにテーブル名も含めて書くならあんまり変わりません
とはいえやっぱり長いし 実際に 「tablename.columnname」 と書くときはエイリアス使って 1, 2 文字のテーブル名です
なのでエイリアスを含めた 「tablealias__columnname」 にしておくのはありな気がしました
列名指定のためのエイリアスが不要になりますし
しかし 「SELECT * FROM ~」 のように全取得してると LEFT JOIN で JOIN 先にマッチする行がないと id が null になってしまう問題に困りました
同じ列名なのでオブジェクトや連想配列やディクショナリとして取得するとあとの方の null で上書きされてしまうのですよね
「*」 を使わず全部書けば解決ですが 長いです
ワイルドカードで除外が選べれば助かるケースはかなりあるのですが SQL では対応してません
それに SQL って全然更新されないので JavaScript などのように待てば便利なる期待もありません
せめて 「select a.* as a__*」 で a__ プレフィックスつけてくれるみたいなのができたらよかったのですけど
それで思ったのが最初から列名を 「tablename__columnname」 みたいなのにしておくことです
見た目はいまいちですが JOIN が入ってくると列名が被る可能性があって 結局 WHERE や SELECT で 「tablename.columnname」 のようにテーブル名も含めて書くならあんまり変わりません
とはいえやっぱり長いし 実際に 「tablename.columnname」 と書くときはエイリアス使って 1, 2 文字のテーブル名です
なのでエイリアスを含めた 「tablealias__columnname」 にしておくのはありな気がしました
列名指定のためのエイリアスが不要になりますし
CREATE TABLE foo (
f__id integer,
f__name text,
PRIMARY KEY (f__id)
);
CREATE TABLE bar (
b__id integer,
b__name text,
b__f_id integer,
PRIMARY KEY (b__id)
);
SELECT * FROM foo LEFT JOIN bar ON f__id = b__f_id WHERE f__id < 10;
クリックした単語を取得しようと思ったのですが思いの外難しいです
ここでは英単語のようにスペース区切りだけを扱って日本語を形態素解析してーとかいう話は無しです
まずクリックした場所の文字を判定する方法
単語ごとに span タグでわかれていないと長い p タグの中からどうやって判定するんだろう?
というところですが これは document.getSelection() を使えば Node とクリックした場所の offset が取得できるので簡単です
困るのはタグが挟まってる場合です
単語の途中で一部だけを色を付けたいとかそういう用途で単語の途中でタグが入る場合があります
タグの前後にスペースがないならつながった文字とみなせば良さそうですが div の場合は改行なのでスペースがなくても区切るべきです
しかし これは div に限ったものではありません
HTML の表示は CSS で変えれるので span でも改行してるかもしれませんし タグが挟まるとつなげてよいかの判断が難しいです
自分で用意した HTML のみが対象ならやりようはありますが任意の HTML でやろうとすると結構難しいです
特殊な場合は扱わないと割り切って タグ内だけで判定したり デフォルトが inline のタグかつ前後にスペースない場合のみ結合した上で判定するくらいがいいのかもしれませんね
ここでは英単語のようにスペース区切りだけを扱って日本語を形態素解析してーとかいう話は無しです
まずクリックした場所の文字を判定する方法
単語ごとに span タグでわかれていないと長い p タグの中からどうやって判定するんだろう?
というところですが これは document.getSelection() を使えば Node とクリックした場所の offset が取得できるので簡単です
困るのはタグが挟まってる場合です
単語の途中で一部だけを色を付けたいとかそういう用途で単語の途中でタグが入る場合があります
タグの前後にスペースがないならつながった文字とみなせば良さそうですが div の場合は改行なのでスペースがなくても区切るべきです
しかし これは div に限ったものではありません
HTML の表示は CSS で変えれるので span でも改行してるかもしれませんし タグが挟まるとつなげてよいかの判断が難しいです
自分で用意した HTML のみが対象ならやりようはありますが任意の HTML でやろうとすると結構難しいです
特殊な場合は扱わないと割り切って タグ内だけで判定したり デフォルトが inline のタグかつ前後にスペースない場合のみ結合した上で判定するくらいがいいのかもしれませんね
Web サーバとバックグラウンド処理を両方やってるとき 共有してる変数の状態が中途半端な状態のものを使って処理してレスポンス返すのを防ぎたい
バックグラウンド処理の途中では Web サーバのリクエストが来ても待機してバックグラウンド処理を実行してないときに Web サーバの処理をしてレスポンス返したい
共有してる変数に Promise を入れてバックグラウンド処理が始まると未解決の Promise をセット
終わったらそれを resolve
リクエストがきたときは 最初にその Promise を await すれば良い
コレくらいにシンプルだといいけど Web サーバでリクエストを処理するときに 最初以外のバックグラウンド処理の待機以外で 途中に await が混ざると await 後はまたバックグラウンド処理を始めてる可能性あり
await が入るならリクエスト処理中はバックグラウンド処理を始めないようにバックグラウンド処理側でもリクエスト処理の await 入れたほうが良さそう
ここまで来ると複雑だし いい感じのライブラリ探してそれに任せたほうがいいかも
バックグラウンド処理の途中では Web サーバのリクエストが来ても待機してバックグラウンド処理を実行してないときに Web サーバの処理をしてレスポンス返したい
共有してる変数に Promise を入れてバックグラウンド処理が始まると未解決の Promise をセット
終わったらそれを resolve
リクエストがきたときは 最初にその Promise を await すれば良い
let count = 0
let waiting = null
let lock_count = 0
let resolve = null
const lock = () => {
lock_count++
console.log("LOCK+", lock_count, count)
if(!waiting) {
waiting = new Promise(r => resolve = r)
}
}
const unlock = () => {
lock_count--
console.log("LOCK-", lock_count, count)
if(lock_count === 0) {
resolve()
waiting = null
}
}
const wait = ms => new Promise(r => setTimeout(r, ms))
// バックグラウンド処理
setInterval(async () => {
lock()
await wait(Math.random() * 5000)
count++
unlock()
}, 3000)
// Web サーバ
require("http").createServer(async (req, res) => {
await waiting
res.end(String(count))
}).listen(9123)
コレくらいにシンプルだといいけど Web サーバでリクエストを処理するときに 最初以外のバックグラウンド処理の待機以外で 途中に await が混ざると await 後はまたバックグラウンド処理を始めてる可能性あり
await が入るならリクエスト処理中はバックグラウンド処理を始めないようにバックグラウンド処理側でもリクエスト処理の await 入れたほうが良さそう
ここまで来ると複雑だし いい感じのライブラリ探してそれに任せたほうがいいかも
◯ EventEmitter の基本的な使い方
毎秒時刻をコンソールに表示
Promise も非同期処理の通知ができるけど一回限り
二回目以降の通知はできず 終了済みの値は保持されて終了してれば待機せず即実行できる
◯ Promise で EventEmitter みたいなことをやってみる
Promise の結果に本来の結果と次の Promise を入れて 結果を受け取ったら次は新しく受け取った Promise の resolve を待機する
await の無限ループにするので then の中でやらないとメインの処理が進まなくなる
Promise の入れ替えなどちょっと面倒
◯ for await of のループを利用する
generator にして Promise を受け取る
for-of を使って Promise の入れ替えを考えなくていいようにする
generator の中で作った関数を公開して その関数の中で yield できたら楽だったけど generator スコープ中でしか yield できない
なので emit に当たる関数が呼び出されるまで待機するには await での待機が必要
async generator にして await で待機してから yield する
毎秒時刻をコンソールに表示
const emitter = new EventEmitter()
emitter.on("aaa", value => console.log(value))
setInterval(() => emitter.emit("aaa", Date.now()), 2000)
Promise も非同期処理の通知ができるけど一回限り
二回目以降の通知はできず 終了済みの値は保持されて終了してれば待機せず即実行できる
◯ Promise で EventEmitter みたいなことをやってみる
Promise の結果に本来の結果と次の Promise を入れて 結果を受け取ったら次は新しく受け取った Promise の resolve を待機する
const notifyPromise = () => {
let resolve
const promise = new Promise(a => (resolve = a))
const notify = value => {
const r = resolve
const p = new Promise(a => (resolve = a))
r([value, p])
}
return [notify, promise]
}
const [notify, promise] = notifyPromise()
Promise.resolve().then(async () => {
let p = promise
while (true) {
const [value, next] = await p
try {
console.log(value)
} finally {
p = next
}
}
})
setInterval(() => notify(Date.now()), 2000)
await の無限ループにするので then の中でやらないとメインの処理が進まなくなる
Promise の入れ替えなどちょっと面倒
◯ for await of のループを利用する
generator にして Promise を受け取る
for-of を使って Promise の入れ替えを考えなくていいようにする
const f = () => {
let resolve
const wait = () => new Promise(r => (resolve = r))
async function* g() {
while (true) yield await wait()
}
const notify = v => resolve(v)
const it = g()
return [notify, it]
}
const [notify, it] = f()
Promise.resolve().then(async () => {
for await (const value of it) {
console.log(value)
}
})
setInterval(() => notify(Date.now()), 2000)
generator の中で作った関数を公開して その関数の中で yield できたら楽だったけど generator スコープ中でしか yield できない
なので emit に当たる関数が呼び出されるまで待機するには await での待機が必要
async generator にして await で待機してから yield する
SQL でグループごとの最大値の行がほしいとき
単純にやると group by して最大値の値を取り出してそれで where するという 2 段階な考え方
window 関数がいいよっという紹介をみたので 調べてみるとグループごとにソートしてランクとか順番のデータの列を作ることができるみたい
便利そうだけど対象テーブルの行全部にランクの列を追加した一時テーブル作ってそこから検索ってあんまり速くなさそう
気になったので実際に試してみた
データは 30000 件ちょっと
id 列が主キーで group_id 列はインデックスあり
PARTITION BY
MAX
plan 的には PARTITION BY のほうが速いけど実際の結果は MAX のほうが速かった
何回かやっても一緒
短いし速いしなら MAX でよさそう
単純にやると group by して最大値の値を取り出してそれで where するという 2 段階な考え方
window 関数がいいよっという紹介をみたので 調べてみるとグループごとにソートしてランクとか順番のデータの列を作ることができるみたい
便利そうだけど対象テーブルの行全部にランクの列を追加した一時テーブル作ってそこから検索ってあんまり速くなさそう
気になったので実際に試してみた
データは 30000 件ちょっと
id 列が主キーで group_id 列はインデックスあり
PARTITION BY
explain analyze
select "id", "group_id"
from (select "id", "group_id", row_number() OVER (PARTITION BY group_id ORDER BY id DESC) as n from "tbl") as "sub"
where "n" = 1
"Subquery Scan on sub (cost=3678.01..4868.84 rows=183 width=8) (actual time=29.683..41.953 rows=3 loops=1)"
" Filter: (sub.n = 1)"
" Rows Removed by Filter: 36705"
" -> WindowAgg (cost=3678.01..4410.83 rows=36641 width=16) (actual time=29.681..40.401 rows=36708 loops=1)"
" -> Sort (cost=3678.01..3769.61 rows=36641 width=8) (actual time=29.671..31.515 rows=36708 loops=1)"
" Sort Key: tbl.group_id, tbl.id DESC"
" Sort Method: quicksort Memory: 3087kB"
" -> Seq Scan on tbl (cost=0.00..900.41 rows=36641 width=8) (actual time=0.024..7.945 rows=36708 loops=1)"
"Planning time: 0.178 ms"
"Execution time: 42.396 ms"
MAX
explain analyze
select * from tbl where id = ANY (select max(id) from tbl group by group_id)
"Nested Loop (cost=1083.97..1108.64 rows=3 width=80) (actual time=15.348..15.352 rows=3 loops=1)"
" -> HashAggregate (cost=1083.68..1083.71 rows=3 width=4) (actual time=15.331..15.332 rows=3 loops=1)"
" Group Key: max(tbl_1.id)"
" -> HashAggregate (cost=1083.62..1083.64 rows=3 width=8) (actual time=15.327..15.328 rows=3 loops=1)"
" Group Key: tbl_1.group_id"
" -> Seq Scan on tbl tbl_1 (cost=0.00..900.41 rows=36641 width=8) (actual time=0.020..5.036 rows=36708 loops=1)"
" -> Index Scan using tbl_pkey on tbl (cost=0.29..8.31 rows=1 width=80) (actual time=0.005..0.005 rows=1 loops=3)"
" Index Cond: (id = (max(tbl_1.id)))"
"Planning time: 0.410 ms"
"Execution time: 15.421 ms"
plan 的には PARTITION BY のほうが速いけど実際の結果は MAX のほうが速かった
何回かやっても一緒
短いし速いしなら MAX でよさそう
async function case1() {
// ...
const result = await something().catch(err => err)
if (result instanceof Error) {
console.error(result)
if (result.display_message) notifyUser(result.display_message)
return
}
// ...
return result.data
}
async function case2() {
// ...
let result
try {
result = await something()
} catch (err) {
console.error(err)
if (err.display_message) notifyUser(err.display_message)
return
}
// ...
return result.data
}
case2 は見づらいので case1 のほうが好き
速度の違いを調べてみる
async function something(n) {
if (n & 1) throw new Error("error")
return { data: "ok" }
}
async function case1(n) {
const result = await something(n).catch(err => err)
if (result instanceof Error) {
if (result.display_message) notifyUser(result.display_message)
return { ok: false, error: result }
}
return { ok: true, value: result.data }
}
async function case2(n) {
let result
try {
result = await something(n)
} catch (err) {
if (err.display_message) notifyUser(err.display_message)
return { ok: false, error: err }
}
return { ok: true, value: result.data }
}
async function measure1() {
console.time(1)
for (let i = 0; i < 10000; i++) {
await case1(i)
}
console.timeEnd(1)
}
async function measure2() {
console.time(2)
for (let i = 0; i < 10000; i++) {
await case2(i)
}
console.timeEnd(2)
}
!async function () {
await measure1()
await measure2()
await measure1()
await measure2()
await measure1()
await measure2()
await measure1()
await measure2()
}()
1: 258.104736328125ms
2: 198.275146484375ms
1: 252.72900390625ms
2: 210.5810546875ms
1: 286.694091796875ms
2: 210.133056640625ms
1: 262.489990234375ms
2: 220.68994140625ms
2 のほうが微妙に速かった
と言っても倍にもなってないし 1 万回で 40ms 程度の差なので気にせず 1 を使うつもり
ただ今回みたいなケースだと これで十分そう
async function case3(n) {
return something(n).then(
result => ({ ok: true, value: result.data }),
err => {
if (err.display_message) notifyUser(result.display_message)
return ({ ok: false, error: err })
}
)
}
成功時の処理が長いならネストが深くなって見づらくなるので case1 の方法のほうが良さそう
case3 の速度は case1 と case2 の間くらい
自分で catch 書きたくなくて ステータスとして成功・失敗を結果に含んでくれればいいのにと思ってたら最近できた Promise.allSettled がいい感じ
async function case4(n) {
const [result] = await Promise.allSettled([something(n)])
if (result.status === "rejected") {
if (result.reason.display_message) notifyUser(result.reason.display_message)
return { ok: false, error: result.reason }
}
return { ok: true, value: result.value.data }
}
ただ 本来は複数の Promise 全部が成功か失敗で終了したときに resolve される Promise を取得する用途なので配列になる
それでも見た目的にはよさそう
肝心の速度は case1 よりもちょっと遅めだった
;(document.getElementById("elem").onclick = () => console.log(1))()
"(" から始めたくないので 関数呼び出しする関数を使う
Function.prototype.call.call(document.getElementById("elem").onclick = () => console.log(1))
そこそこちゃんとしたものだと addEventListener にしたいけど addEventListener は返り値なし
remove することも考えるとリスナ関数を返してほしいのでそういう関数を作っとく
const listen = (elem, type, fn, options) => (elem.addEventListener(type, fn, options), fn)
Function.prototype.call.call(listen(document.getElementById("elem"), "click", () => console.log(1)))
関数作るくらいなら登録兼呼び出しする関数作ってしまうほうが楽
const listenAndCall = (elem, type, fn, options) => (fn(), elem.addEventListener(type, fn, options))
前に書いたやつに関連して
constructor を書きたくないときの対処方法でこんなの思いついた
継承する関数だから constructor として実行される
constructor をクラス定義に書かなくていいいし
書き方的に一番上に固定されるから constructor 探す手間がはぶける
本来のプロトタイプで作るみたいに constructor とそのプロパティを別に分けられる
結構いいところが多いんだけど 関数を継承してるので 特定のクラスを継承したクラスを作れないデメリットあり
constructor を書きたくない理由は
◯ 単語が長い上に打ちづらい
◯ 他のメソッドと分けたい
◯ super() 書かないといけないのが面倒
クラスプロパティの記法だと defineProperty になって setter を使った代入にならない
constructor を書きたくないときの対処方法でこんなの思いついた
class C extends function(opt) {
this.x = opt.x
this.y = opt.y
} {
show() { console.log(this.x, this.y) }
}
new C({x: 1, y: 2}).show()
// 1 2
継承する関数だから constructor として実行される
constructor をクラス定義に書かなくていいいし
書き方的に一番上に固定されるから constructor 探す手間がはぶける
本来のプロトタイプで作るみたいに constructor とそのプロパティを別に分けられる
結構いいところが多いんだけど 関数を継承してるので 特定のクラスを継承したクラスを作れないデメリットあり
constructor を書きたくない理由は
◯ 単語が長い上に打ちづらい
◯ 他のメソッドと分けたい
◯ super() 書かないといけないのが面倒
クラスプロパティの記法だと defineProperty になって setter を使った代入にならない
class X (opt) {
x = opt.x
y = opt.y
show() { console.log(this.x, this.y) }
}
こんな テンプレートがあって
c の div の表示条件を追加して useC 変数が true のときのみ表示するようにしたい
すごく見づらい
かと言って if 文を使うなら 文が書けないといけないので関数の即時実行になって その中で if 文なのでもっと見づらい
{} が増えてネストや行数が増える
別のところで関数定義しておけばシンプルにはなるけど 別のところに書いてるのでテンプレートだけを見て理解できないから微妙
相当複雑でもないならテンプレート中に分岐の条件とかあってほしい
タグの html に条件を指定できて こうなってたら少しましかも
でもこれだと htmlIf の出力が切り替わるだけで c() の実行は常に行われる
表示しないときは表示用のデータがないこともあって エラーが出ないように工夫が必要になってくる
いらないはずの処理のために余計なことはしたくない
となるとやっぱり分岐か関数にして実行を遅延させるか
関数にするとこういう感じ
条件演算子よりマシな気もするけど なんかイマイチ
全体だとこうなってる
やっぱり一般的なテンプレートみたいな書き方がベスト
これが一番見やすい
ただテンプレートリテラルの使用上 埋め込む処理は事前計算されるから実現不可
こういう構文にすればできるけど c() のところみたいに関数にしないとダメだし html は自作のものにして特別な事前処理が必要になるからやりたくない
ネストしない意味では hidden みたいな感じで使えるといいんだけどなー
html`
<div>
<div class="a">
${a()}
</div>
<div class="b">
${b()}
</div>
<div class="c">
${c()}
</div>
</div>
`
c の div の表示条件を追加して useC 変数が true のときのみ表示するようにしたい
html`
<div>
<div class="a">
${a()}
</div>
<div class="b">
${b()}
</div>
${useC
? html`
<div class="c">
${c()}
</div>
`
: null
}
</div>
`
すごく見づらい
かと言って if 文を使うなら 文が書けないといけないので関数の即時実行になって その中で if 文なのでもっと見づらい
{} が増えてネストや行数が増える
別のところで関数定義しておけばシンプルにはなるけど 別のところに書いてるのでテンプレートだけを見て理解できないから微妙
相当複雑でもないならテンプレート中に分岐の条件とかあってほしい
タグの html に条件を指定できて こうなってたら少しましかも
${
htmlIf(useC)`
<div class="c">
${c()}
</div>
`
}
でもこれだと htmlIf の出力が切り替わるだけで c() の実行は常に行われる
表示しないときは表示用のデータがないこともあって エラーが出ないように工夫が必要になってくる
いらないはずの処理のために余計なことはしたくない
となるとやっぱり分岐か関数にして実行を遅延させるか
関数にするとこういう感じ
callIf(useC, () => html`
<div class="c">
${c()}
</div>
`)
条件演算子よりマシな気もするけど なんかイマイチ
全体だとこうなってる
html`
<div>
<div class="a">
${a()}
</div>
<div class="b">
${b()}
</div>
${callIf(useC, () => html`
<div class="c">
${c()}
</div>
`)}
</div>
`
やっぱり一般的なテンプレートみたいな書き方がベスト
html`
<div>
<div class="a">
${a()}
</div>
<div class="b">
${b()}
</div>
{IF useC}
<div class="c">
${c()}
</div>
{/IF}
</div>
`
これが一番見やすい
ただテンプレートリテラルの使用上 埋め込む処理は事前計算されるから実現不可
html`
<div>
<div class="a">
${a()}
</div>
<div class="b">
${b()}
</div>
${IF(useC)}
<div class="c">
${() => c()}
</div>
${ENDIF}
</div>
`
こういう構文にすればできるけど c() のところみたいに関数にしないとダメだし html は自作のものにして特別な事前処理が必要になるからやりたくない
ネストしない意味では hidden みたいな感じで使えるといいんだけどなー
html`
<div>
<div class="a">
${a()}
</div>
<div class="b">
${b()}
</div>
<div if=${useC}>
${c()}
</div>
</div>
`
Chrome が余計な変更をしたので data: から始まる URL はユーザが直接入力しない限りトップフレームで開けません
かと言って URL.createObjectURL を使って あとからメモリ解放もするというのはあまりしたくないです
トップフレームじゃなければ開けるので全画面で iframe を使うようにすると見た目上はほぼ同じにできます
実際にトップフレームで開く用の HTML を用意します
このページのクエリパラメータで 「?url=data:~」 を指定します
単純に URL を iframe で開くので iframe で開くことを禁止してるページでなければ https の URL でも使えます
DataURI は長くなりがちなので URL の長さの限界を超えたいなら window.open した返り値の window オブジェクトを保持して
かと言って URL.createObjectURL を使って あとからメモリ解放もするというのはあまりしたくないです
トップフレームじゃなければ開けるので全画面で iframe を使うようにすると見た目上はほぼ同じにできます
実際にトップフレームで開く用の HTML を用意します
<!doctype html>
<iframe></iframe>
<style>
body {
margin: 0;
}
iframe {
border: 0;
padding: 0;
margin: 0;
width: 100vw;
height: 100vh;
display: block;
}
</style>
<script>
const url = new URL(location).searchParams.get("url")
if (url) document.querySelector("iframe").src = url
</script>
このページのクエリパラメータで 「?url=data:~」 を指定します
単純に URL を iframe で開くので iframe で開くことを禁止してるページでなければ https の URL でも使えます
DataURI は長くなりがちなので URL の長さの限界を超えたいなら window.open した返り値の window オブジェクトを保持して
const subwin = window.open("/preview.html")
subwin.contentDocument.querySelector("iframe").src = url