POST するときのボディのサイズの話
nginx も Fastify もデフォルトは 1MB なので 10MB まで許可にしたいときに両方を変更しないといけないです
二重に管理するのは無駄ですし 片方だけしか更新してなかったみたいな設定漏れが出るようにしか思わないです
なので外側の nginx で制御して Node.js 側は無制限にしようとしました
ですが Fastify ではサイズを無制限にできないようです
サーバー全体やルート単位で bodyLimit を設定できますが 0 以下や Number.isInteger が false になるものは不正な設定として弾かれます
なので 1 以上の整数にしかできず これと必ず比較するので無制限にならないです
0 は無制限というありそうな設定はないですし Infinity も設定できません
Number.MAX_SAFE_INTEGER を設定しておけばいいといえばいいのですが なんかしっくりこないです
リバースプロキシを配置することを推奨するなら こういうところを快適にしてほしいものです
nginx を置かずに使ってると Node.js 側での制限が必須なので特になんとも思わなかったですが nginx を置いてみて気付いた不満点です
直接自分で Fastify インスタンスを作るところならまだいいですが Fastify CLI を使うとなると コマンドラインオプションで bodyLimit を指定することになります
コマンドラインで Number.MAX_SAFE_INTEGER 的なものを指定って結構面倒です
完全に一致しなくてもいいので 適当に 1TB などにしておくにしても 0 の数が多くなると見づらいです
Fastify CLI を使うと app.js というルートプラグインが各プラグインを autoload する作りになっています
app.js はこういうの
https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/app.js
基本は Fastify CLI から使われるのみですが この app.js をプラグインとして使うこともできます
主にテストなどで使われます
https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/test/helper.js
テスト以外でも app のインスタンスを作ってそこからグローバルのプラグインで追加したメソッドを使うと便利かなと思ったのですが
これで foo が見つからないエラーでした
foo は plugins フォルダ内のプラグインで decorate しています
原因は Fastify のインスタンスに app.js のプラグインを register するときにカプセル化を有効にしていることでした
カプセル化されると app.js の内部では参照できますが 外側からは参照できません
app.js のデフォルトでは上のコードの通り プラグインの関数は fastify-plugin でラップされていません
ここはあまりいじることを想定されてないみたいですし ラップしないのが推奨なようです
実際 他のアプリと混ぜて同じサーバーで動かしたいときなどでは プラグイン全体をカプセル化して特定のプレフィックス以下に配置したいユースケースはありそうですし ラップしないのは正しい気がします
ですが 今回みたいにラップして 外側から参照できるようにカプセル化しないようにしたい場合もあります
となると app.js を使う側の Fastify CLI で fastify-plugin を通してほしいです
オプションで切り替えれるのかなと探したのですが そういうのはないようでした
https://github.com/fastify/fastify-cli/blob/v6.1.1/start.js#L149
現状だと仕方ないので app.js 側で fastify-plugin を通すしかなさそうです
他アプリと混ぜて使う想定がないなら常に fastify-plugin を通しても問題なさそうですが 環境変数を見て変えるということもできそうです
[app.js]
app.js はこういうの
https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/app.js
import path from 'path'
import AutoLoad from '@fastify/autoload'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Pass --options via CLI arguments in command to enable these options.
export const options = {}
export default async function (fastify, opts) {
// Place here your custom code!
// Do not touch the following lines
// This loads all plugins defined in plugins
// those should be support plugins that are reused
// through your application
fastify.register(AutoLoad, {
dir: path.join(__dirname, 'plugins'),
options: Object.assign({}, opts)
})
// This loads all plugins defined in routes
// define your routes in one of these
fastify.register(AutoLoad, {
dir: path.join(__dirname, 'routes'),
options: Object.assign({}, opts)
})
}
基本は Fastify CLI から使われるのみですが この app.js をプラグインとして使うこともできます
主にテストなどで使われます
https://github.com/fastify/fastify-cli/blob/v6.1.1/templates/app-esm/test/helper.js
テスト以外でも app のインスタンスを作ってそこからグローバルのプラグインで追加したメソッドを使うと便利かなと思ったのですが
import helper from "fastify-cli/helper.js"
const app = await helper.build([path_to_app_js], {})
app.foo()
これで foo が見つからないエラーでした
foo は plugins フォルダ内のプラグインで decorate しています
原因は Fastify のインスタンスに app.js のプラグインを register するときにカプセル化を有効にしていることでした
カプセル化されると app.js の内部では参照できますが 外側からは参照できません
app.js のデフォルトでは上のコードの通り プラグインの関数は fastify-plugin でラップされていません
ここはあまりいじることを想定されてないみたいですし ラップしないのが推奨なようです
実際 他のアプリと混ぜて同じサーバーで動かしたいときなどでは プラグイン全体をカプセル化して特定のプレフィックス以下に配置したいユースケースはありそうですし ラップしないのは正しい気がします
ですが 今回みたいにラップして 外側から参照できるようにカプセル化しないようにしたい場合もあります
となると app.js を使う側の Fastify CLI で fastify-plugin を通してほしいです
オプションで切り替えれるのかなと探したのですが そういうのはないようでした
https://github.com/fastify/fastify-cli/blob/v6.1.1/start.js#L149
現状だと仕方ないので app.js 側で fastify-plugin を通すしかなさそうです
他アプリと混ぜて使う想定がないなら常に fastify-plugin を通しても問題なさそうですが 環境変数を見て変えるということもできそうです
[app.js]
// 前略
const App = async function (fastify, opts) {
// 略
}
if (!process.env.ENCAPSULATE_APP) {
App = fp(App)
}
export default App
preload するスクリプト用のコマンドライン引数という珍しいものを見ました
みたいなもので foo=bar は pre.js 用という扱いでした
pre.js で処理した process.argv は main.js でも共通なので pre.js で先に引数を処理してしまうみたいです
例
[pre.js]
[main.js]
pre.js で splice を使って引数の最初の 2 個を取り出して main.js ではそれ以外が残ってる感じです
順番だけでなく pre.js 側で特定のプレフィックスのみを処理するとかもあると思います
preload をあまり使わないので気にしてませんでしたが 結構ありな考え方ですね
node --import pre.js main.js foo=bar
みたいなもので foo=bar は pre.js 用という扱いでした
pre.js で処理した process.argv は main.js でも共通なので pre.js で先に引数を処理してしまうみたいです
例
[pre.js]
const args = process.argv.splice(2, 2)
console.log("pre", args)
[main.js]
console.log("main", process.argv)
root@fcb8f9998337:/mnt/t/1# node --import ./pre.js main.js a b c
pre [ 'a', 'b' ]
main [ '/usr/local/bin/node', '/mnt/tmp/1/main.js', 'c' ]
pre.js で splice を使って引数の最初の 2 個を取り出して main.js ではそれ以外が残ってる感じです
順番だけでなく pre.js 側で特定のプレフィックスのみを処理するとかもあると思います
preload をあまり使わないので気にしてませんでしたが 結構ありな考え方ですね
Node.js で 1 行標準入力から読み取ってなにかの処理をして を繰り返すとき よく使うのが標準モジュールの readline です
簡単に使えますが 単にループで読み取るだけだとプロンプトは自動で出力されません
これを実行して文字を入力すると エンターを押すたびに { line: "入力値" } のようなのが表示されます
入力するとき何も表示されていないので入力していい状態なのか分かりづらいです
createInterface の prompt の初期値は "> " になっているので紛らわしいのですが 手動で prompt メソッドを呼び出さないとプロンプトは出力されません
また output を標準出力に指定していないと prompt メソッドを呼び出しても画面に表示できません
こうすることで表示されます
ただこれだと prompt の呼び出しが分かれますし なんか気持ち悪いです
自分でこれを書きたくないし 自動で内部的にやってほしいものなので asyncIterator を置き換えることにしました
[auto-prompt.js]
autoPrompt 側は仕方ないとして 普段使う部分ではスッキリと書けます
標準入力を使うならこの使い方で十分そうですが 全インスタンスに反映させるなら Interface の prototype の方を書き換えることもできます
簡単に使えますが 単にループで読み取るだけだとプロンプトは自動で出力されません
import readline from "node:readline"
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
for await (const line of rl) {
console.log({ line })
}
これを実行して文字を入力すると エンターを押すたびに { line: "入力値" } のようなのが表示されます
user@DESKTOP03 ~> node rl.js
a
{ line: 'a' }
123
{ line: '123' }
入力するとき何も表示されていないので入力していい状態なのか分かりづらいです
createInterface の prompt の初期値は "> " になっているので紛らわしいのですが 手動で prompt メソッドを呼び出さないとプロンプトは出力されません
また output を標準出力に指定していないと prompt メソッドを呼び出しても画面に表示できません
こうすることで表示されます
import readline from "node:readline"
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
rl.prompt()
for await (const line of rl) {
console.log({ line })
rl.prompt()
}
user@DESKTOP03 ~> node rl.js
> 1
{ line: '1' }
> xx
{ line: 'xx' }
> ⏎
ただこれだと prompt の呼び出しが分かれますし なんか気持ち悪いです
自分でこれを書きたくないし 自動で内部的にやってほしいものなので asyncIterator を置き換えることにしました
[auto-prompt.js]
const org_async_iterator = Symbol("org-async-iterator")
async function* asyncIterator(...args) {
this.prompt()
for await (const item of this[org_async_iterator].call(this, ...args)) {
yield item
this.prompt()
}
}
const autoPrompt = (rl) => {
rl[org_async_iterator] = rl[Symbol.asyncIterator]
rl[Symbol.asyncIterator] = asyncIterator
}
export default autoPrompt
import readline from "node:readline"
import autoPrompt from "./auto-prompt.js"
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
autoPrompt(rl)
for await (const line of rl) {
console.log({ line })
}
user@DESKTOP03 ~> node rl.js
> foo
{ line: 'foo' }
> bar
{ line: 'bar' }
> ⏎
autoPrompt 側は仕方ないとして 普段使う部分ではスッキリと書けます
標準入力を使うならこの使い方で十分そうですが 全インスタンスに反映させるなら Interface の prototype の方を書き換えることもできます
autoPrompt(readline.Interface.prototype)
Fastify の CLI を使うと自分が書く部分はプラグインになります
プロセスのエントリポイント部分はライブラリ側で行われるのでウェブサーバーとしての処理しか書けないです
普段は Node.js のウェブサーバーをたてるときは ウェブサーバーとしての処理以外にもプロセス内でいろいろなことをしています
index.js ではそれらをセットアップして その中の一つとしてウェブサーバーもあるという感じです
ウェブサーバー以外のものは 例えば DB やログ等の定期的なクリア処理です
古いデータを消したりローテートしたりを 24 時間間隔などで行います
Node.js はタイマー処理がしやすいので cron 等に任せるより楽です
また ウェブサーバー以外で外部サービスからの通知類を受信したりすることもあります
Redis や PostgreSQL の通知の仕組みだったり 専用の TCP 通信だったりで外部と通信しています
受けたデータによってウェブサーバー側の処理を変更したりしますし DB からキャッシュを再取得するとかもあるので 同じプロセスの方が都合がいいです
別プロセスに分けると ウェブサーバーに API を設けて内部的にそこにアクセスして変更を伝えるようなことになり二度手間感があります
反対にウェブサーバーが受けたリクエストに応じて 通知を受信してる処理を変更するケースもあります
外部通信の ON/OFF の切り替えなどをするならやっぱり同じプロセスの方が扱いやすいです
分けることもあるのですが それでもウェブサーバー側が親で外部通信系が子となる関係で リクエストに応じて子プロセスを止めたり起動したりというのが多いです
systemd でそれぞれ別サービスにすることもできますが 分けすぎても扱いづらいのと ウェブサーバーのプロセスから外部通信プロセスを再起動したいみたいなときに systemd を通すと面倒なのですよね
以前は PM2 を通して systemd は PM2 を起動して ウェブサーバーは PM2 と通信して別プロセスを制御してましたが systemd だけで済ませられたほうが楽です
そうなると systemd がウェブサーバーを含むプロセスだけ起動して そこで全部をやるか一部をそのプロセスのサブプロセスにするかになります
他にも WebSocket サーバーも使うなら それらを統合する必要がありますし ロガーや DB のコネクションなどをウェブサーバーと WebSocket サーバーで共有するなら一括してそれらの外側で作って管理したいです
そんなわけで Fastify CLI って簡単なものでウェブサーバーだけならいいのですが 他にもやりたい場合に不便に感じます
でも PHP などはこういう状態になるわけですし 全部分けて別プロセスって割り切るほうが良いこともあるのでしょうか
Node.js はリクエスト間で変数の空間が分かれないので積極的にキャッシュしていけるのが魅力ですが プロセスが分かれるとキャッシュの更新ができずその利点が活かせなくなるのが困るところなんですよね
プロセスのエントリポイント部分はライブラリ側で行われるのでウェブサーバーとしての処理しか書けないです
普段は Node.js のウェブサーバーをたてるときは ウェブサーバーとしての処理以外にもプロセス内でいろいろなことをしています
index.js ではそれらをセットアップして その中の一つとしてウェブサーバーもあるという感じです
ウェブサーバー以外のものは 例えば DB やログ等の定期的なクリア処理です
古いデータを消したりローテートしたりを 24 時間間隔などで行います
Node.js はタイマー処理がしやすいので cron 等に任せるより楽です
また ウェブサーバー以外で外部サービスからの通知類を受信したりすることもあります
Redis や PostgreSQL の通知の仕組みだったり 専用の TCP 通信だったりで外部と通信しています
受けたデータによってウェブサーバー側の処理を変更したりしますし DB からキャッシュを再取得するとかもあるので 同じプロセスの方が都合がいいです
別プロセスに分けると ウェブサーバーに API を設けて内部的にそこにアクセスして変更を伝えるようなことになり二度手間感があります
反対にウェブサーバーが受けたリクエストに応じて 通知を受信してる処理を変更するケースもあります
外部通信の ON/OFF の切り替えなどをするならやっぱり同じプロセスの方が扱いやすいです
分けることもあるのですが それでもウェブサーバー側が親で外部通信系が子となる関係で リクエストに応じて子プロセスを止めたり起動したりというのが多いです
systemd でそれぞれ別サービスにすることもできますが 分けすぎても扱いづらいのと ウェブサーバーのプロセスから外部通信プロセスを再起動したいみたいなときに systemd を通すと面倒なのですよね
以前は PM2 を通して systemd は PM2 を起動して ウェブサーバーは PM2 と通信して別プロセスを制御してましたが systemd だけで済ませられたほうが楽です
そうなると systemd がウェブサーバーを含むプロセスだけ起動して そこで全部をやるか一部をそのプロセスのサブプロセスにするかになります
他にも WebSocket サーバーも使うなら それらを統合する必要がありますし ロガーや DB のコネクションなどをウェブサーバーと WebSocket サーバーで共有するなら一括してそれらの外側で作って管理したいです
そんなわけで Fastify CLI って簡単なものでウェブサーバーだけならいいのですが 他にもやりたい場合に不便に感じます
でも PHP などはこういう状態になるわけですし 全部分けて別プロセスって割り切るほうが良いこともあるのでしょうか
Node.js はリクエスト間で変数の空間が分かれないので積極的にキャッシュしていけるのが魅力ですが プロセスが分かれるとキャッシュの更新ができずその利点が活かせなくなるのが困るところなんですよね
Node.js の前段に nginx を置いたほうがいいのか 新しくサーバー環境を作るたびに考えてる問題です
これまでは nginx は無しで Node.js で全部やることにしていました
Node.js 内で全部やるほうが柔軟にできますし サーバーの追加は面倒です
間に追加のレイヤーが増えるほうが複雑化しますし 処理が増える以上パフォーマンス面でも劣りますし 単純に管理するものも増えます
設定ミスやバグなどでうまく動かないとかが出る可能性はありますし シンプル化できるものはシンプルにしておきたいという考えです
ただ Fastify では 前段に nginx などを置くことを推奨しています
たまには置いてみようかな ということで試してました
やってみると たしかに nginx を使ったほうが 圧縮とかキャッシュとか証明書や TLS バージョン等の HTTPS 関係やアクセスログなどを Node.js 側でやらなくていいので Node.js 側は楽になりました
静的ファイルも nginx に任せてしまうと Node.js は完全に API だけに専念できるので いつも入れてるライブラリ類も結構減りました
Node.js 側をシンプルにするという意味では良さそうです
Node.js で全部やると柔軟にできるといっても フレームワークの都合で うまくやりたいように設定できなくて遠回りに自分で制御してたりしたので これでいいような気もしてきました
静的ファイルのサーブとルートの処理の優先順とか SPA に対応させるとか フォルダごとにキャッシュ設定変えたりとか こういうことって結構面倒だったりしますし
それにフレームワークを変えるコストも減ります
Node.js ですべてやると静的ファイルのサーブやレスポンスの圧縮などの方法はフレームワークごとに違いますし ミドルウェアやプラグインを自分で入れて設定しないといけないです
これが結構面倒で 新しいフレームワークを使うときのハードルでもありました
ですが この辺を nginx 側に移すと Node.js 側でやらなくていいのでフレームワークの変更が楽になります
もちろん nginx を別のリバースプロキシのウェブサーバーに変えることはありえるのですが Node.js 側のフレームワークに比べるとめったにないです
フレームワークはコードを実装するのに大きく関連するところなので良さそうなものがあれば積極的に変えたいですが nginx の部分は必要な機能が動いてればそんなに変えたいものでもないです
一応 その他のもので使われてるものは Fastify のページで紹介されてるものだと HAProxy で他のところでは Caddy や lighttpd があるようでした
特にこれらに置き換えたい理由もないですし Node.js のフレームワークよりも次から次に出てくるツールではないと思います
また nginx 側を見ると最初こそ設定ファイルの見づらさはありますが 慣れると結構シンプルで読みやすいと思います
server や location みたいなブロックがあってそれぞれのコンテキストの中に書ける設定が決まっています
location は一つだけにマッチするので ネストすることで全体的な設定と個別の設定を書きます
設定は 「key value;」 形式の行からなるものでドキュメントに一覧があります
Node.js でのフレームワークごとのミドルウェア・プラグインの設定を探すよりもわかりやすいと思います
パフォーマンス的には劣るかもですが そもそもそんなに速度に困ってはないのでわずかに遅くなったくらいで影響はないので その他のメリットが多ければ別にいいかなというところです
それに静的ファイルを nginx で処理すれば その部分は Node.js より速くなるはずです
残る API の処理はもともと少し時間がかかることが多いところなので nginx が増えることによる時間程度は本当に誤差です
静的ファイルの処理を nginx に移すことでユーザー情報が参照できなくて URL を知ってれば誰でもアクセスできてしまいますが PHP だってずっとそれなわけですし 頑張れば画面が見れるくらいで実際のデータは API の認証が必要になるので別にいい気がします
あとこれは Node.js を直接公開してる場合が前提になってます
クラウドを使うみたいなことがあれば別にロードバランサーがあったりすると思うので そうなると Node.js 用の nginx とかはなくしてそっちに任せるでいいと思います
ロードバランサーでは静的ファイルのサーブはしてくれないかもですが クラウドなら CDN を使うことが多いそうですし
そもそもクラウドになると Lambda や Cloud Functions みたいなのがあってあまりサーバー自体を用意しないのかもしれませんけど
簡単に試してただけだと nginx で特に不満は無いと思ってましたが 少し使ってるとちょっとした不満点がありました
圧縮フォーマットの brotli に標準で対応していません
もちろん最近の zstd もです
古い gzip しか使えません
Node.js でも標準で brotli に対応してたので その点効率が落ちるのは残念かも
一応外部モジュールで追加はできるらしいのですが dnf などで簡単には導入できないみたいです
RHEL 系は epel でインストールできる風に書いてるページもあったのですが AlmaLinux9 ではパッケージが見つからなかったです
そんなに手間を掛けてまで入れたいわけではないので gzip で妥協していますが 最近は安定していてあまり新しい機能は追加されてないようなので将来性考えるとどうなのかなと思い始めました
不満点はもう一つあって JavaScript の mime type が application/javascript です
application/javascript はすでに廃止されていて text/javascript が正式です
https://datatracker.ietf.org/doc/html/rfc9239#name-iana-considerations
なのに普段使わないような古い形式になっていて 設定では application/javascript と書かないといけないです
つい text/javascript と書いて動かなかったことがありました
設定変えるなんてめったにないし ほとんどコピペなのですが やっぱりこういうのは気になるのですよね
これまでは nginx は無しで Node.js で全部やることにしていました
Node.js 内で全部やるほうが柔軟にできますし サーバーの追加は面倒です
間に追加のレイヤーが増えるほうが複雑化しますし 処理が増える以上パフォーマンス面でも劣りますし 単純に管理するものも増えます
設定ミスやバグなどでうまく動かないとかが出る可能性はありますし シンプル化できるものはシンプルにしておきたいという考えです
ただ Fastify では 前段に nginx などを置くことを推奨しています
たまには置いてみようかな ということで試してました
やってみると たしかに nginx を使ったほうが 圧縮とかキャッシュとか証明書や TLS バージョン等の HTTPS 関係やアクセスログなどを Node.js 側でやらなくていいので Node.js 側は楽になりました
静的ファイルも nginx に任せてしまうと Node.js は完全に API だけに専念できるので いつも入れてるライブラリ類も結構減りました
Node.js 側をシンプルにするという意味では良さそうです
Node.js で全部やると柔軟にできるといっても フレームワークの都合で うまくやりたいように設定できなくて遠回りに自分で制御してたりしたので これでいいような気もしてきました
静的ファイルのサーブとルートの処理の優先順とか SPA に対応させるとか フォルダごとにキャッシュ設定変えたりとか こういうことって結構面倒だったりしますし
それにフレームワークを変えるコストも減ります
Node.js ですべてやると静的ファイルのサーブやレスポンスの圧縮などの方法はフレームワークごとに違いますし ミドルウェアやプラグインを自分で入れて設定しないといけないです
これが結構面倒で 新しいフレームワークを使うときのハードルでもありました
ですが この辺を nginx 側に移すと Node.js 側でやらなくていいのでフレームワークの変更が楽になります
もちろん nginx を別のリバースプロキシのウェブサーバーに変えることはありえるのですが Node.js 側のフレームワークに比べるとめったにないです
フレームワークはコードを実装するのに大きく関連するところなので良さそうなものがあれば積極的に変えたいですが nginx の部分は必要な機能が動いてればそんなに変えたいものでもないです
一応 その他のもので使われてるものは Fastify のページで紹介されてるものだと HAProxy で他のところでは Caddy や lighttpd があるようでした
特にこれらに置き換えたい理由もないですし Node.js のフレームワークよりも次から次に出てくるツールではないと思います
また nginx 側を見ると最初こそ設定ファイルの見づらさはありますが 慣れると結構シンプルで読みやすいと思います
server や location みたいなブロックがあってそれぞれのコンテキストの中に書ける設定が決まっています
location は一つだけにマッチするので ネストすることで全体的な設定と個別の設定を書きます
設定は 「key value;」 形式の行からなるものでドキュメントに一覧があります
Node.js でのフレームワークごとのミドルウェア・プラグインの設定を探すよりもわかりやすいと思います
パフォーマンス的には劣るかもですが そもそもそんなに速度に困ってはないのでわずかに遅くなったくらいで影響はないので その他のメリットが多ければ別にいいかなというところです
それに静的ファイルを nginx で処理すれば その部分は Node.js より速くなるはずです
残る API の処理はもともと少し時間がかかることが多いところなので nginx が増えることによる時間程度は本当に誤差です
静的ファイルの処理を nginx に移すことでユーザー情報が参照できなくて URL を知ってれば誰でもアクセスできてしまいますが PHP だってずっとそれなわけですし 頑張れば画面が見れるくらいで実際のデータは API の認証が必要になるので別にいい気がします
あとこれは Node.js を直接公開してる場合が前提になってます
クラウドを使うみたいなことがあれば別にロードバランサーがあったりすると思うので そうなると Node.js 用の nginx とかはなくしてそっちに任せるでいいと思います
ロードバランサーでは静的ファイルのサーブはしてくれないかもですが クラウドなら CDN を使うことが多いそうですし
そもそもクラウドになると Lambda や Cloud Functions みたいなのがあってあまりサーバー自体を用意しないのかもしれませんけど
簡単に試してただけだと nginx で特に不満は無いと思ってましたが 少し使ってるとちょっとした不満点がありました
圧縮フォーマットの brotli に標準で対応していません
もちろん最近の zstd もです
古い gzip しか使えません
Node.js でも標準で brotli に対応してたので その点効率が落ちるのは残念かも
一応外部モジュールで追加はできるらしいのですが dnf などで簡単には導入できないみたいです
RHEL 系は epel でインストールできる風に書いてるページもあったのですが AlmaLinux9 ではパッケージが見つからなかったです
そんなに手間を掛けてまで入れたいわけではないので gzip で妥協していますが 最近は安定していてあまり新しい機能は追加されてないようなので将来性考えるとどうなのかなと思い始めました
不満点はもう一つあって JavaScript の mime type が application/javascript です
application/javascript はすでに廃止されていて text/javascript が正式です
https://datatracker.ietf.org/doc/html/rfc9239#name-iana-considerations
なのに普段使わないような古い形式になっていて 設定では application/javascript と書かないといけないです
つい text/javascript と書いて動かなかったことがありました
設定変えるなんてめったにないし ほとんどコピペなのですが やっぱりこういうのは気になるのですよね
pino の redact を見てると 「〇」 が使われてた
https://github.com/davidmarkclements/fast-redact/blob/v3.4.0/lib/validator.js
じゃあ〇を使うとエラーになる?と 思い付きで試してみました
pino と redact の使い方としてはこういうの
ログするオブジェクトのパスを複数指定できて 指定したプロパティが伏せられます
〇 を入れてみると
確かにエラーになってます
ログをしなくてもインスタンスの作成時点でエラーです
少し特殊な
になってるのは記号の対処のためで 内部でプロパティをどう処理してるかというと
でコードを作ってそれを実行しています
eval みたいなものです
expr が .prop みたいになってます
foo.bar は有効ですが foo.1 や foo.! は無効です
JavaScript として有効なアクセス方法になるように [1] や ["!"] みたいにしないといけないです
そのため少し特殊な書き方になってます
記号などでも ["!"] の書き方にすると動作します
〇 の記号はいくつかあって fast-redact に使われてるのは一番上のものです
〇 U+3007
◯ U+25EF
○ U+25CB
他の 〇 ならエラーになりません
https://github.com/davidmarkclements/fast-redact/blob/v3.4.0/lib/validator.js
じゃあ〇を使うとエラーになる?と 思い付きで試してみました
pino と redact の使い方としてはこういうの
> require("pino")({ redact: ["key"] }).info({ key: "foo" })
{"level":30,"time":1710331939803,"pid":54,"hostname":"67cfbc893e4a","key":"[Redacted]"}
> require("pino")({ redact: ["あ"] }).info({ あ: "foo" })
{"level":30,"time":1710331978251,"pid":54,"hostname":"67cfbc893e4a","あ":"[Redacted]"}
ログするオブジェクトのパスを複数指定できて 指定したプロパティが伏せられます
〇 を入れてみると
> require("pino")({ redact: [`["〇"]`] })
Uncaught Error: pino – redact paths array contains an invalid path (["〇"])
at /mnt/x8uc1/node_modules/fast-redact/lib/validator.js:29:15
at Array.forEach (<anonymous>)
at validate (/mnt/x8uc1/node_modules/fast-redact/lib/validator.js:12:11)
at handle (/mnt/x8uc1/node_modules/pino/lib/redaction.js:107:5)
at redaction (/mnt/x8uc1/node_modules/pino/lib/redaction.js:16:29)
at pino (/mnt/x8uc1/node_modules/pino/pino.js:129:33)
確かにエラーになってます
ログをしなくてもインスタンスの作成時点でエラーです
少し特殊な
[`["〇"]`]
になってるのは記号の対処のためで 内部でプロパティをどう処理してるかというと
o${expr}
でコードを作ってそれを実行しています
eval みたいなものです
expr が .prop みたいになってます
foo.bar は有効ですが foo.1 や foo.! は無効です
JavaScript として有効なアクセス方法になるように [1] や ["!"] みたいにしないといけないです
そのため少し特殊な書き方になってます
記号などでも ["!"] の書き方にすると動作します
> require("pino")({ redact: ["!"] }).info({ "!": "a" })
Uncaught Error: pino – redact paths array contains an invalid path (!)
(略)
> require("pino")({ redact: [`["!"]`] }).info({ "!": "a" })
{"level":30,"time":1710352889569,"pid":350,"hostname":"8f9aa7f09ea2","!":"[Redacted]"}
〇 の記号はいくつかあって fast-redact に使われてるのは一番上のものです
〇 U+3007
◯ U+25EF
○ U+25CB
他の 〇 ならエラーになりません
> const fn = k => {
... console.log(k, k.charCodeAt().toString(16))
... require("pino")({ redact: [`["${k}"]`] }).info({ [k]: "a" })
... }
> fn("〇")
〇 3007
Uncaught Error: pino – redact paths array contains an invalid path (["〇"])
(略)
> fn("◯")
◯ 25ef
{"level":30,"time":1710416813795,"pid":3966,"hostname":"67cfbc893e4a","◯":"[Redacted]"}
> fn("○")
○ 25cb
{"level":30,"time":1710416820183,"pid":3966,"hostname":"67cfbc893e4a","○":"[Redacted]"}
Node.js を ESM にしてから不便に感じてる部分はやっぱりファイルパスの解決部分です
CJS のときはこんな感じで書けた部分ですが
[/tmp/a.js]
この長さになります
[/tmp/b.js]
Node.js 20 なら import.meta に resolve があって __dirname を作らなくてもそのファイルからの相対パスでフルパスを取得できます
でも file:// から始まる URL 形式なので最後に fileURLToPath を通してローカルのパス形式にしないといけないです
[/tmp/c.js]
Node.js 21 なら import.meta.dirname があるので CJS と同じようなことができます
🔗 Node.js 21.2 で ESM に CJS の __dirname と __filename 相当の機能が追加された
[/tmp/d.js]
でも現在の LTS は 20 です
21 に機能追加されて 3 ヶ月以上経っても 20 にバックポートされないのであまり期待できなそうです
使えるのは 22 の LTS からになるかもしれません
それに CJS と近い感じで書けるだけで __dirname が import.meta.dirname になって少し長いです
fs が URL 形式のパスもサポートしてくれるともっと楽なんですけどね
それなら同じフォルダのファイルを読み取るときはこれだけで済みます
そういう話がないのか探してみたのですが あまり積極的ではないようで放置されて issue がクローズされてました
https://github.com/nodejs/node/issues/48994
単に中で fileURLToPath を通すだけで file://foo/bar みたいなものを入れてもエラーにするでいいと思うのですけど
CJS のときはこんな感じで書けた部分ですが
[/tmp/a.js]
const path = require("path")
const file_path = path.join(__dirname, "./file.txt")
console.log(file_path)
// /tmp/file.txt
この長さになります
[/tmp/b.js]
import { fileURLToPath } from "node:url"
import path from "node:path"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const file_path = path.join(__dirname, "./file.txt")
console.log(file_path)
// /tmp/file.txt
Node.js 20 なら import.meta に resolve があって __dirname を作らなくてもそのファイルからの相対パスでフルパスを取得できます
でも file:// から始まる URL 形式なので最後に fileURLToPath を通してローカルのパス形式にしないといけないです
[/tmp/c.js]
import { fileURLToPath } from "node:url"
const file_url = import.meta.resolve("./file.txt")
console.log(file_url)
// file:///tmp/file.txt
const file_path = fileURLToPath(file_url)
console.log(file_path)
// /tmp/file.txt
Node.js 21 なら import.meta.dirname があるので CJS と同じようなことができます
🔗 Node.js 21.2 で ESM に CJS の __dirname と __filename 相当の機能が追加された
[/tmp/d.js]
import path from "node:path"
const file_path = path.join(import.meta.dirname, "./file.txt")
console.log(file_path)
// /tmp/file.txt
でも現在の LTS は 20 です
21 に機能追加されて 3 ヶ月以上経っても 20 にバックポートされないのであまり期待できなそうです
使えるのは 22 の LTS からになるかもしれません
それに CJS と近い感じで書けるだけで __dirname が import.meta.dirname になって少し長いです
fs が URL 形式のパスもサポートしてくれるともっと楽なんですけどね
それなら同じフォルダのファイルを読み取るときはこれだけで済みます
fs.readFileSync(import.meta.resolve("./file.txt"))
そういう話がないのか探してみたのですが あまり積極的ではないようで放置されて issue がクローズされてました
https://github.com/nodejs/node/issues/48994
単に中で fileURLToPath を通すだけで file://foo/bar みたいなものを入れてもエラーにするでいいと思うのですけど
Fastify では printRoutes や printPlugins で登録したルートやプラグインをわかりやすく表示できます
printPlugins ではプラグインの名前が表示されます
ただ register のところで import を使うと問題が出ます
import が返すのは Promise オブジェクトなので [object Promise] という表示になってました
register の引数のところで import を書くのは Fastify 側でサポートされていて ドキュメントでも見かける方法なのに対応できてないのは残念です
ただ import が終わって Promise が解決されないと中身の情報がわからないです
printPlugins が呼び出されるときに Promise が解決済みとも限らないですし仕方ないものなのかもしれません
名前をつけてみたら動くかなと試したら動きました
考えてみたら .name を参照してますが Promise だと .name はないはずです
どうやって作ってるのか気になったのでソースを見てみました
https://github.com/fastify/avvio/blob/v8.3.0/lib/get-plugin-name.js
いくつかパターンがあるみたいです
Promise のプロパティを追加しなくても単純にオプションとして name を渡すで良さそうです
デバッグなど開発時向け機能の printPlugins のためだけに名前をつける必要があるのかはわかりませんけど 一時的にどれなのかわからないからつけるというときには良さそうです
printPlugins ではプラグインの名前が表示されます
import Fastify from "fastify"
import sensible from "@fastify/sensible"
const fastify = Fastify()
await fastify.register(sensible)
console.log(fastify.printPlugins())
root -1 ms
├── bound _after 8 ms
├── @fastify/sensible 1 ms
└── bound _after 0 ms
ただ register のところで import を使うと問題が出ます
import Fastify from "fastify"
const fastify = Fastify()
await fastify.register(import("@fastify/sensible"))
console.log(fastify.printPlugins())
root -1 ms
├── bound _after 6 ms
├── [object Promise] 1 ms
└── bound _after 1 ms
import が返すのは Promise オブジェクトなので [object Promise] という表示になってました
register の引数のところで import を書くのは Fastify 側でサポートされていて ドキュメントでも見かける方法なのに対応できてないのは残念です
ただ import が終わって Promise が解決されないと中身の情報がわからないです
printPlugins が呼び出されるときに Promise が解決済みとも限らないですし仕方ないものなのかもしれません
名前をつけてみたら動くかなと試したら動きました
import Fastify from "fastify"
const importWithName = (specifier, name) =>
Object.assign(import(specifier), { name: name || specifier })
const fastify = Fastify()
await fastify.register(importWithName("@fastify/sensible", "sensible"))
console.log(fastify.printPlugins())
root -1 ms
├── bound _after 8 ms
├── sensible 1 ms
└── bound _after 0 ms
考えてみたら .name を参照してますが Promise だと .name はないはずです
どうやって作ってるのか気になったのでソースを見てみました
https://github.com/fastify/avvio/blob/v8.3.0/lib/get-plugin-name.js
いくつかパターンがあるみたいです
Promise のプロパティを追加しなくても単純にオプションとして name を渡すで良さそうです
import Fastify from "fastify"
const fastify = Fastify()
await fastify.register(import("@fastify/sensible"), { name: "sensible" })
console.log(fastify.printPlugins())
root -1 ms
├── bound _after 4 ms
├── sensible 1 ms
└── bound _after 0 ms
デバッグなど開発時向け機能の printPlugins のためだけに名前をつける必要があるのかはわかりませんけど 一時的にどれなのかわからないからつけるというときには良さそうです
Node.js を使っていて構文エラーがあったのですが原因の場所がわからなかったです
いつもならファイルと行番号を表示してくれるのですが なぜか表示されません
使ってるツールの都合なのかといろいろ試してたら 単純に Node.js を使うだけでも再現できました
mjs ファイルを動的にロードするとダメみたいです
拡張子によるものではないので type="module" でも同じですがここではわかりやすくするため cjs/mjs の拡張子にします
ロードされる mod.cjs / mod.mjs はどっちも構文エラーになるものです
[mod.cjs]
[mod.mjs]
これらをロードする main.cjs / main.mjs を作ります
[main.cjs]
[main.mjs]
main のそれぞれを実行します
どちらの場合でも mjs ファイルをロードしたときは構文エラーの場所が表示されていませんね
構文エラーならエディタ上ですぐにわかるのであまり困らないのですが 今回は高機能なエディタがない環境だったので苦戦しました
ちなみに「動的」なインポートでのみ起きます
静的な import の場合はちゃんとエラーの場所が表示されます
[main-s.mjs]
Node.js バージョンは 18, 20, 21 で確認しましたがどれもこうなるようです
いつもならファイルと行番号を表示してくれるのですが なぜか表示されません
使ってるツールの都合なのかといろいろ試してたら 単純に Node.js を使うだけでも再現できました
mjs ファイルを動的にロードするとダメみたいです
拡張子によるものではないので type="module" でも同じですがここではわかりやすくするため cjs/mjs の拡張子にします
ロードされる mod.cjs / mod.mjs はどっちも構文エラーになるものです
[mod.cjs]
console.log(1
[mod.mjs]
console.log(1
これらをロードする main.cjs / main.mjs を作ります
[main.cjs]
try {
require("./mod.cjs")
} catch(err) {
console.log("cjs error:", { err })
}
import("./mod.mjs").catch(err => console.log("mjs error:", { err }))
[main.mjs]
await import("./mod.cjs").catch(err => console.log("cjs error:", { err }))
await import("./mod.mjs").catch(err => console.log("mjs error:", { err }))
main のそれぞれを実行します
root@e07f755b37f2:/opt# node main.cjs
cjs error: {
err: /opt/mod.cjs:1
console.log(1
^
SyntaxError: missing ) after argument list
at internalCompileFunction (node:internal/vm:77:18)
at wrapSafe (node:internal/modules/cjs/loader:1290:20)
at Module._compile (node:internal/modules/cjs/loader:1342:27)
at Module._extensions..js (node:internal/modules/cjs/loader:1437:10)
at Module.load (node:internal/modules/cjs/loader:1212:32)
at Module._load (node:internal/modules/cjs/loader:1028:12)
at Module.require (node:internal/modules/cjs/loader:1237:19)
at require (node:internal/modules/helpers:176:18)
at Object.<anonymous> (/opt/main.cjs:2:5)
at Module._compile (node:internal/modules/cjs/loader:1378:14)
}
mjs error: {
err: SyntaxError: missing ) after argument list
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:168:18)
at callTranslator (node:internal/modules/esm/loader:279:14)
at ModuleLoader.moduleProvider (node:internal/modules/esm/loader:285:30)
at async link (node:internal/modules/esm/module_job:76:21)
}
root@e07f755b37f2:/opt# node main.mjs
cjs error: {
err: /opt/mod.cjs:1
console.log(1
^
SyntaxError: missing ) after argument list
at internalCompileFunction (node:internal/vm:77:18)
at wrapSafe (node:internal/modules/cjs/loader:1290:20)
at Module._compile (node:internal/modules/cjs/loader:1342:27)
at Module._extensions..js (node:internal/modules/cjs/loader:1437:10)
at Module.load (node:internal/modules/cjs/loader:1212:32)
at Module._load (node:internal/modules/cjs/loader:1028:12)
at cjsLoader (node:internal/modules/esm/translators:359:17)
at ModuleWrap.<anonymous> (node:internal/modules/esm/translators:308:7)
at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
at async ModuleLoader.import (node:internal/modules/esm/loader:323:24)
}
mjs error: {
err: SyntaxError: missing ) after argument list
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:168:18)
at callTranslator (node:internal/modules/esm/loader:279:14)
at ModuleLoader.moduleProvider (node:internal/modules/esm/loader:285:30)
at async link (node:internal/modules/esm/module_job:76:21)
}
どちらの場合でも mjs ファイルをロードしたときは構文エラーの場所が表示されていませんね
構文エラーならエディタ上ですぐにわかるのであまり困らないのですが 今回は高機能なエディタがない環境だったので苦戦しました
ちなみに「動的」なインポートでのみ起きます
静的な import の場合はちゃんとエラーの場所が表示されます
[main-s.mjs]
import "./mod.mjs"
root@e07f755b37f2:/opt# node main-s.mjs
file:///opt/mod.mjs:1
console.log(1
^
SyntaxError: missing ) after argument list
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:168:18)
at callTranslator (node:internal/modules/esm/loader:279:14)
at ModuleLoader.moduleProvider (node:internal/modules/esm/loader:285:30)
Node.js バージョンは 18, 20, 21 で確認しましたがどれもこうなるようです
Windows 環境で 公式サイトからダウンロードしたインストーラーを使って Node.js をインストールした後に npx を使うとエラーが出ました
no such file or directory
Sandbox 下で Node.js だけをインストールして試すと再現します
バージョンは LTS の 20 系です
メッセージのまま AppData\Roaming\npm が無いらしいのですが 無いなら作って欲しいのに作ってくれないみたいです
手動で作ってもいいですが適当になにかのパッケージを npm でグローバルインストールすると作られます
no such file or directory
Sandbox 下で Node.js だけをインストールして試すと再現します
バージョンは LTS の 20 系です
npm ERR! code ENOENT
npm ERR! syscall lstat
npm ERR! path C:\Users\WDAGUtilityAccount\AppData\Roaming\npm
npm ERR! errno -4058
npm ERR! enoent ENOENT: no such file or directory, lstat 'C:\Users\WDAGUtilityAccount\AppData\Roaming\npm'
npm ERR! enoent This is related to npm not being able to find a file.
npm ERR! enoent
メッセージのまま AppData\Roaming\npm が無いらしいのですが 無いなら作って欲しいのに作ってくれないみたいです
手動で作ってもいいですが適当になにかのパッケージを npm でグローバルインストールすると作られます
npm -g i (適当なパッケージ名)
Welcome to Node.js v21.4.0.
Type ".help" for more information.
> fs.writeFileSync("foo/bar/file", "text")
undefined
> await fs.promises.readdir("foo/bar")
[ 'file' ]
> await fs.promises.readdir("foo/bar", { withFileTypes: true })
[
Dirent {
name: 'file',
parentPath: 'foo/bar',
path: 'foo/bar',
[Symbol(type)]: 1
}
]
path と一緒ですね
どういう場合に違うんだろうといろいろ試しても違いがわからないのでドキュメントを読むとエイリアスだそうです
どうして全く同じものを?と思いましたが元々は path という名前でした
なのに実体は親のパスです
Dirent に対して path と言われたら name も含めたフルパスを期待するのに readdir の引数に渡されたパスになっています
これが紛らわしいので path を置き換える目的で追加されたようです
情報がないエラーが出ていて困ったのですが cause が出ていないだけでした
inner の情報が表示されません
クリックで開けるのかと思いましたがそういうこともできませんでした
cause に限らず AggregateError の errors プロパティも同様に表示されません
console.log で出ている変数なら右クリックから "Store object as global variable" でグローバル変数に持ってきてから自分で error.cause にアクセスすれば情報が見れるかと思ったのですが エラーオブジェクトはなぜかグローバル変数に持ってこれないみたいです
すでにログされたものはどうしようもなさそうです
対処方法はログ方法を変更して console.dir を使うことです
HTMLElement 等を XML 表示にせずオブジェクト表示させるのに使うものです
エラー表示もそれらと同じ扱いみたいで console.dir でオブジェクト表示を強制すると内側もプロパティも表示されるようになります
調べてみると 2 年以上前から要望として issue はあるものの対応されてない状態みたいです
https://bugs.chromium.org/p/chromium/issues/detail?id=1211260
ちなみに Node.js の場合は inspect で内部プロパティを表示してくれるので console.dir にせず通常の console.log でいいです
console.log(new Error("error", { cause: new Error("inner") }))
// Error: error
// at <anonymous>:1:13
inner の情報が表示されません
クリックで開けるのかと思いましたがそういうこともできませんでした
cause に限らず AggregateError の errors プロパティも同様に表示されません
console.log で出ている変数なら右クリックから "Store object as global variable" でグローバル変数に持ってきてから自分で error.cause にアクセスすれば情報が見れるかと思ったのですが エラーオブジェクトはなぜかグローバル変数に持ってこれないみたいです
すでにログされたものはどうしようもなさそうです
対処方法はログ方法を変更して console.dir を使うことです
HTMLElement 等を XML 表示にせずオブジェクト表示させるのに使うものです
エラー表示もそれらと同じ扱いみたいで console.dir でオブジェクト表示を強制すると内側もプロパティも表示されるようになります
console.dir(new Error("error", { cause: new Error("inner") }))
// Error: error
// at <anonymous>:1:13
// cause: Error: inner at <anonymous>:1:41
// message: "inner"
// stack: "Error: inner\n at <anonymous>:1:41"
// [[Prototype]]: Object
// message: "error"
// stack: "Error: error\n at <anonymous>:1:13"
// [[Prototype]]: Object
調べてみると 2 年以上前から要望として issue はあるものの対応されてない状態みたいです
https://bugs.chromium.org/p/chromium/issues/detail?id=1211260
ちなみに Node.js の場合は inspect で内部プロパティを表示してくれるので console.dir にせず通常の console.log でいいです
> console.log(new Error("error", { cause: new Error("inner") }))
Error: error
at REPL47:1:13
at ContextifyScript.runInThisContext (node:vm:121:12)
... 7 lines matching cause stack trace ...
at [_line] [as _line] (node:internal/readline/interface:887:18) {
[cause]: Error: inner
at REPL47:1:41
at ContextifyScript.runInThisContext (node:vm:121:12)
at REPLServer.defaultEval (node:repl:599:22)
at bound (node:domain:432:15)
at REPLServer.runBound [as eval] (node:domain:443:12)
at REPLServer.onLine (node:repl:929:10)
at REPLServer.emit (node:events:526:35)
at REPLServer.emit (node:domain:488:12)
at [_onLine] [as _onLine] (node:internal/readline/interface:416:12)
at [_line] [as _line] (node:internal/readline/interface:887:18)
}
Node.js 21 で組み込みの fetch が安定しましたが 20 からたいして変更なさそうだしと 20 でも使ってました
それで動作が違うところがあり バージョンの違いで動作が違う部分があったのかと思ったのですが 20 で揃えても違ってました
違った部分はエラーが起きたときのエラーオブジェクトです
片方はよく見る感じのエラーです
もう片方は cause の中が AggregateError になっていて 二つのエラーが含まれていました
同じエラーでなぜ 2 個分含まれているのか疑問でしたがよく見ると address が IPv6 と IPv4 で別になっていました
IPv6 が有効になっている環境で IPv6 で接続できないと自動で IPv4 でも試してくれてるみたいです
ちなみに OS は同じ AlmaLinux9 だったのですが IPv6 が有効な環境と無効な環境がありました
それで動作が違うところがあり バージョンの違いで動作が違う部分があったのかと思ったのですが 20 で揃えても違ってました
違った部分はエラーが起きたときのエラーオブジェクトです
片方はよく見る感じのエラーです
fetch("http://localhost").catch(console.err)
Uncaught TypeError: fetch failed
at Object.fetch (node:internal/deps/undici/undici:11730:11)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
cause: Error: connect ECONNREFUSED 127.0.0.1:80
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1595:16)
at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
errno: -111,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '127.0.0.1',
port: 80
}
}
もう片方は cause の中が AggregateError になっていて 二つのエラーが含まれていました
同じエラーでなぜ 2 個分含まれているのか疑問でしたがよく見ると address が IPv6 と IPv4 で別になっていました
IPv6 が有効になっている環境で IPv6 で接続できないと自動で IPv4 でも試してくれてるみたいです
ちなみに OS は同じ AlmaLinux9 だったのですが IPv6 が有効な環境と無効な環境がありました
以前は shell script のファイルをダウンロードして bash にパイプする方法でした
実行すると環境に応じた repo ファイルがダウンロードされて /etc/yum.repos.d/ に配置される形でした
それがリポジトリを追加するためのパッケージをインストールする方法になっていました
rpm をインストールするとリポジトリが追加されるという形です
epel などと同じです
こっちの方が扱いやすくて良いですね
例えば Node.js 20 の場合は
という感じです
URL の 20 のところを 18 や 21 にすることで別バージョンに切り替えできます
これでリポジトリが追加されたので nodejs パッケージをインストールしようとすると 指定バージョンの Node.js がインストールできます
実行すると環境に応じた repo ファイルがダウンロードされて /etc/yum.repos.d/ に配置される形でした
それがリポジトリを追加するためのパッケージをインストールする方法になっていました
rpm をインストールするとリポジトリが追加されるという形です
epel などと同じです
こっちの方が扱いやすくて良いですね
例えば Node.js 20 の場合は
dnf install https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm
という感じです
URL の 20 のところを 18 や 21 にすることで別バージョンに切り替えできます
これでリポジトリが追加されたので nodejs パッケージをインストールしようとすると 指定バージョンの Node.js がインストールできます
関連
🔗 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 なのにプロパティを直接参照できるのでなにか方法があるのかと思ったのですが なさそうでした
組み込みモジュールだからこそ特別な対応がされているのでしょうか
Node.js でプログラムを実行した際に メインモジュールとして実行されたかを判断したいです
メインモジュールとして実行されたときだけ追加の処理をして それ以外のライブラリとして読み込まれたときは何もしないという感じに使います
よく Python で見る
をやりたいです
CJS の頃はシンプルな方法で実現できました
ドキュメントにも記載されている方法です
https://nodejs.org/docs/latest-v20.x/api/modules.html#accessing-the-main-module
しかし 現状の ESM だとこれを簡単に実現する方法はないです
Deno では import.meta.main に true/false でメインかどうかが入っています
同様の機能を実装する issue はあるのですが 2019 年からあるのに未だに実装されません
https://github.com/nodejs/node/issues/49440
__dirname や __filename に相当する機能は実装されたのでこれもそろそろ対応してほしいものです
現状でこれをやろうとするとコマンドラインの引数と比較するという気持ちの悪い方法に頼るしかないです
process.argv[1] と import.meta.url の比較になります
ただコマンドライン引数の場合 パスの解決が必要ですし コマンドラインで指定するものでは拡張子や index.js を省略できるなどもあり 簡単な === では済まないです
なのでこれをうまくやってくれるだけのパッケージが存在します
https://github.com/tschaub/es-main
現時点でスターが 71 で 週間ダウンロード数が 3 万以上です
これだけの需要があるのになぜ標準で実装しないのが疑問です
一部のメンバーが反対してるようなのですが 実装したところでデメリットなんて無いでしょうし CJS の頃からよく使われてるもので JavaScript 外でも使われるような方法なのに 何が気に入らないのでしょうね
メインモジュールとして実行されたときだけ追加の処理をして それ以外のライブラリとして読み込まれたときは何もしないという感じに使います
よく Python で見る
if __name__ == "__main__":
...
をやりたいです
CJS の頃はシンプルな方法で実現できました
if (require.main === module) {
// ...
}
ドキュメントにも記載されている方法です
https://nodejs.org/docs/latest-v20.x/api/modules.html#accessing-the-main-module
しかし 現状の ESM だとこれを簡単に実現する方法はないです
Deno では import.meta.main に true/false でメインかどうかが入っています
同様の機能を実装する issue はあるのですが 2019 年からあるのに未だに実装されません
https://github.com/nodejs/node/issues/49440
__dirname や __filename に相当する機能は実装されたのでこれもそろそろ対応してほしいものです
現状でこれをやろうとするとコマンドラインの引数と比較するという気持ちの悪い方法に頼るしかないです
process.argv[1] と import.meta.url の比較になります
ただコマンドライン引数の場合 パスの解決が必要ですし コマンドラインで指定するものでは拡張子や index.js を省略できるなどもあり 簡単な === では済まないです
なのでこれをうまくやってくれるだけのパッケージが存在します
https://github.com/tschaub/es-main
現時点でスターが 71 で 週間ダウンロード数が 3 万以上です
これだけの需要があるのになぜ標準で実装しないのが疑問です
一部のメンバーが反対してるようなのですが 実装したところでデメリットなんて無いでしょうし CJS の頃からよく使われてるもので JavaScript 外でも使われるような方法なのに 何が気に入らないのでしょうね
https://nodejs.org/api/esm.html#importmetadirname
https://github.com/nodejs/node/pull/48740
import.meta.filename
import.meta.dirname
で取得できます
これまでは import.meta.url から自身のファイルのパスを file:// 形式で取得してローカルパス形式に変換する必要がありました
これを毎回書くのが面倒だったのでかなり便利になりますね
ところで 18 や 20 でも使いたいなと思って Polyfill できないか考えてみました
こんなものを作ってみたのですが 期待通りには動かなかったです
import.meta ってグローバルオブジェクト風に見えて 特殊なもので モジュールごとに別の実体があるのでどこかでプロパティを追加しても他のモジュールには影響しないです
プロトタイプのないオブジェクトなので プロトタイプの方を拡張することもできません
loader を使って全モジュールの最初に import.meta.filename などを追加する方法は取れなくもないですが ソースコードを暗黙的に書き換えるようなことはあまりしたくないですし 諦めてバックポートを待とうと思います(需要的に安定すればきっとされるはず)
https://github.com/nodejs/node/pull/48740
import.meta.filename
import.meta.dirname
で取得できます
root@cca30b68b828:/# cat /tmp/a.js
console.log(import.meta.filename)
console.log(import.meta.dirname)
root@cca30b68b828:/# node --experimental-detect-module /tmp/a.js
/tmp/a.js
/tmp
これまでは import.meta.url から自身のファイルのパスを file:// 形式で取得してローカルパス形式に変換する必要がありました
import path from "node:path"
import url from "node:url"
const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
これを毎回書くのが面倒だったのでかなり便利になりますね
ところで 18 や 20 でも使いたいなと思って Polyfill できないか考えてみました
こんなものを作ってみたのですが 期待通りには動かなかったです
import url from "node:url"
import path from "node:path"
if (!import.meta.filename) {
Object.defineProperties(import.meta, {
filename: {
get() {
return url.fileURLToPath(this.url)
},
},
dirname: {
get() {
return path.dirname(this.filename)
},
},
})
}
import.meta ってグローバルオブジェクト風に見えて 特殊なもので モジュールごとに別の実体があるのでどこかでプロパティを追加しても他のモジュールには影響しないです
プロトタイプのないオブジェクトなので プロトタイプの方を拡張することもできません
loader を使って全モジュールの最初に import.meta.filename などを追加する方法は取れなくもないですが ソースコードを暗黙的に書き換えるようなことはあまりしたくないですし 諦めてバックポートを待とうと思います(需要的に安定すればきっとされるはず)
Node.20 の LTS 化は来週みたい
Node.js 21 の新機能
https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V21.md#21.0.0
ブラウザ互換の WebSocket クライアントが追加されたみたいです
まだ実験的なのでフラグが必要です
Node.js をクライアントとして使いたいケースはパッと思いつかないですが テストには便利かも
fetch が stable になったみたいです
18 からフラグなしで使えてたのでもう普通に使ってましたが そういえばまだ experimental でしたね
glob 機能が追加されたみたいです
ただテスト関連の機能で内部的に使ってるだけみたいで公開されてないので fs モジュールをインポートしても直接使うことはできないようです
glob 機能だけでも使えるようになるといいですね
一番気になるところは --experimental-default-type オプションの追加
これでデフォルトを ESM に変更できます
これまではコマンド実行時に ESM と指定できず 拡張子を .mjs にしたり package.json を作って type に module を指定しないといけなくて面倒でした
これで ESM が使いやすくなる と思ったのですが これには問題もあるようです
ここのサイトで議論されている内容など詳しく説明されてました
https://jser.info/2023/10/18/node.js-roadmap-esm-by-default/
自分のコードの範囲だけ考えてましたが デフォルトが変わると node_modules のパッケージにも影響があります
古いパッケージは package.json に type が記載されていなくて CJS で書かれているので デフォルトが ESM になるとエラーになるということみたいです
たしかにそうですね
でも 現状で困っていてデフォルトを変えたいのは package.json を使わないスクリプトを実行するときです
npm パッケージは使わず 自分で書いたモジュールを読み込むときです
対象を package.json が無いところだけ にして node_modules の中の package.json があるところはこれまで通りで type を見て 無ければ CJS でいいと思うんです
package.json を作るところなら type: module の記載をするようにすればいいですし パッケージマネージャーなどが package.json を作るときにデフォルトで type: module になっていれば不便に思うところも無い気がします
package.json があって type が無いものまで ESM にするのだと 過去パッケージは使えないことにするしかないですし 中身を見て判断するというのはパフォーマンスが悪くなるなどが理由で .cjs と .mjs の拡張子に分けると決まったときに拒否されてたはずです
Node.js 21 の新機能
https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V21.md#21.0.0
ブラウザ互換の WebSocket クライアントが追加されたみたいです
まだ実験的なのでフラグが必要です
Node.js をクライアントとして使いたいケースはパッと思いつかないですが テストには便利かも
fetch が stable になったみたいです
18 からフラグなしで使えてたのでもう普通に使ってましたが そういえばまだ experimental でしたね
glob 機能が追加されたみたいです
ただテスト関連の機能で内部的に使ってるだけみたいで公開されてないので fs モジュールをインポートしても直接使うことはできないようです
glob 機能だけでも使えるようになるといいですね
一番気になるところは --experimental-default-type オプションの追加
これでデフォルトを ESM に変更できます
これまではコマンド実行時に ESM と指定できず 拡張子を .mjs にしたり package.json を作って type に module を指定しないといけなくて面倒でした
これで ESM が使いやすくなる と思ったのですが これには問題もあるようです
ここのサイトで議論されている内容など詳しく説明されてました
https://jser.info/2023/10/18/node.js-roadmap-esm-by-default/
自分のコードの範囲だけ考えてましたが デフォルトが変わると node_modules のパッケージにも影響があります
古いパッケージは package.json に type が記載されていなくて CJS で書かれているので デフォルトが ESM になるとエラーになるということみたいです
たしかにそうですね
でも 現状で困っていてデフォルトを変えたいのは package.json を使わないスクリプトを実行するときです
npm パッケージは使わず 自分で書いたモジュールを読み込むときです
対象を package.json が無いところだけ にして node_modules の中の package.json があるところはこれまで通りで type を見て 無ければ CJS でいいと思うんです
package.json を作るところなら type: module の記載をするようにすればいいですし パッケージマネージャーなどが package.json を作るときにデフォルトで type: module になっていれば不便に思うところも無い気がします
package.json があって type が無いものまで ESM にするのだと 過去パッケージは使えないことにするしかないですし 中身を見て判断するというのはパフォーマンスが悪くなるなどが理由で .cjs と .mjs の拡張子に分けると決まったときに拒否されてたはずです
Node.js ってけっこう互換性は保たれてるので メジャーアップデートしても問題なく動くことがほとんどでした
特にフロント側は Webpack や Parcel や Vite などでビルドするだけなのでバージョンの違いはほぼ影響ないです
なので フロント側のビルドではプロジェクトごとに Node.js バージョンの管理はしてないです
Windows に入れてる共通の Node.js でビルドしてます
新機能が使いたくなったら LTS の範囲で更新してるものです
最近はそこまで使いたい新機能もなかったので その環境では Node.js 16 が EOL になる少し前くらいに Node.js 18 にしました
それからしばらくして 1 年近くビルドしてなかったプロジェクトを少し更新してビルドしようとしたところエラーになってました
node_modules は前回のビルド以降更新してなくてどうしてエラーが起きるのか最初は原因がわからなかったのですが 調べてみると Node.js のバージョンが原因でした
Node.js 16 から Node.js 18 に上げると package.json の exports フィールドの動きに違いが出ます
Node.js 18 では exports で公開してるもの以外は直接 import/require できなくなります
このせいで一部のライブラリ間の読み込みで失敗していました
正確に書くと postcss 系です
postcss の exports に書かれていないファイルを postcss 系のパッケージのモジュールが直接読み込もうとしてエラーでした
バージョンアップで解決されるはずですが こういう内部で依存関係として自動的にインストールされているものって そこだけを更新し辛いのですよね
このプロジェクトは全体的に古いもので構成されていて できるだけ変更はしたくないです
いつもはとりあえず全部最新にしてるのであまり困らないですが 古いもので最低限の更新だけするというのはやりづらいです
lock ファイルで問題のパッケージのバージョンだけ書き換えることも考えましたが 依存関係が複雑なところだとおかしくなることもありそうで直接触るのは避けたいです
ビルドにしか使わないなら EOL でも別にいいので Node.js のバージョンを古いままにするのが正解なのかもですね
Docker を使って古いバージョンの Node.js でビルドしてみました
ビルドはファイルの読み取りが多い処理なのでファイルが Windows 側にあるとかなり遅いです
もしかすると コンテナ内でリポジトリをクローンしてビルドして push するほうが早いかもしれません
特にフロント側は Webpack や Parcel や Vite などでビルドするだけなのでバージョンの違いはほぼ影響ないです
なので フロント側のビルドではプロジェクトごとに Node.js バージョンの管理はしてないです
Windows に入れてる共通の Node.js でビルドしてます
新機能が使いたくなったら LTS の範囲で更新してるものです
最近はそこまで使いたい新機能もなかったので その環境では Node.js 16 が EOL になる少し前くらいに Node.js 18 にしました
それからしばらくして 1 年近くビルドしてなかったプロジェクトを少し更新してビルドしようとしたところエラーになってました
node_modules は前回のビルド以降更新してなくてどうしてエラーが起きるのか最初は原因がわからなかったのですが 調べてみると Node.js のバージョンが原因でした
Node.js 16 から Node.js 18 に上げると package.json の exports フィールドの動きに違いが出ます
Node.js 18 では exports で公開してるもの以外は直接 import/require できなくなります
このせいで一部のライブラリ間の読み込みで失敗していました
正確に書くと postcss 系です
postcss の exports に書かれていないファイルを postcss 系のパッケージのモジュールが直接読み込もうとしてエラーでした
バージョンアップで解決されるはずですが こういう内部で依存関係として自動的にインストールされているものって そこだけを更新し辛いのですよね
このプロジェクトは全体的に古いもので構成されていて できるだけ変更はしたくないです
いつもはとりあえず全部最新にしてるのであまり困らないですが 古いもので最低限の更新だけするというのはやりづらいです
lock ファイルで問題のパッケージのバージョンだけ書き換えることも考えましたが 依存関係が複雑なところだとおかしくなることもありそうで直接触るのは避けたいです
ビルドにしか使わないなら EOL でも別にいいので Node.js のバージョンを古いままにするのが正解なのかもですね
Docker を使って古いバージョンの Node.js でビルドしてみました
ビルドはファイルの読み取りが多い処理なのでファイルが Windows 側にあるとかなり遅いです
もしかすると コンテナ内でリポジトリをクローンしてビルドして push するほうが早いかもしれません
StackBlitz で Backend 側テンプレートを見てると 見慣れないものがありました
Egg.js
https://github.com/eggjs/egg
Feathers
https://github.com/feathersjs/feathers
H3
https://github.com/unjs/h3
Nitro
https://github.com/unjs/nitro
ラインナップには Express や Koa はあるのに Hapi や Fastify はありません
それらよりもここにあるのは有名なのでしょうか?
ただ Egg.js と Feathers は言われてみると結構前にも見た覚えがなんとなくあります
2018 年か 2019 年くらいにフレームワークを探してたときだったかもです
たしか Egg.js は Koa を中で使ってて 中国でよく使われてるやつだったかと思います
Feathers はリアルタイム系らしいので WebSocket 系のようです
ソースコードを見てみると Socket.IO を使っていました
今でも StackBlitz のテンプレートに並ぶくらいには使われてるのでしょうか
H3 と Nitro は聞き覚えがないので新しいものかもしれません
リリース履歴をみるとどちらも去年からみたいなので新しいようです
H3 の方は Minimal H(TTP) framework らしく 小さめのライブラリみたいです
ミドルウェア系で Express/Koa に近い感じです
Router がついてるので Koa よりは高機能かもしれません
Express と Koa の違いみたいにハンドラで受け取るオブジェクトが異なっています
ただ Express と互換性のある形にもできるそうです
そうしないと Koa があまり流行らなかったみたいなことになりそうですからね
Nitro の方は Nuxt 関連のもののようです
Nuxt を強化できるらしいですが Nuxt なしでも使えるようで Nuxt を使う場合と使わない場合で設定の書き方が分かれてました
どれも今のところは特に使うことなさそうです
Egg.js
https://github.com/eggjs/egg
Feathers
https://github.com/feathersjs/feathers
H3
https://github.com/unjs/h3
Nitro
https://github.com/unjs/nitro
ラインナップには Express や Koa はあるのに Hapi や Fastify はありません
それらよりもここにあるのは有名なのでしょうか?
ただ Egg.js と Feathers は言われてみると結構前にも見た覚えがなんとなくあります
2018 年か 2019 年くらいにフレームワークを探してたときだったかもです
たしか Egg.js は Koa を中で使ってて 中国でよく使われてるやつだったかと思います
Feathers はリアルタイム系らしいので WebSocket 系のようです
ソースコードを見てみると Socket.IO を使っていました
今でも StackBlitz のテンプレートに並ぶくらいには使われてるのでしょうか
H3 と Nitro は聞き覚えがないので新しいものかもしれません
リリース履歴をみるとどちらも去年からみたいなので新しいようです
H3 の方は Minimal H(TTP) framework らしく 小さめのライブラリみたいです
ミドルウェア系で Express/Koa に近い感じです
Router がついてるので Koa よりは高機能かもしれません
Express と Koa の違いみたいにハンドラで受け取るオブジェクトが異なっています
ただ Express と互換性のある形にもできるそうです
そうしないと Koa があまり流行らなかったみたいなことになりそうですからね
Nitro の方は Nuxt 関連のもののようです
Nuxt を強化できるらしいですが Nuxt なしでも使えるようで Nuxt を使う場合と使わない場合で設定の書き方が分かれてました
どれも今のところは特に使うことなさそうです
こういう JavaScript ファイルと ShellScript ファイルがあります
[job.sh]
[job.js]
job.js は常駐するプロセスです
ここでは setInterval でプロセスが終了しないようにしています
これを job.sh から起動します
job.sh は別の Node.js プロセスから起動します
job.sh を起動する側はこんな感じです
job.sh や job.js の出力を受け取りたいので stdout の標準出力にリスナをつけています
job.js は常駐しますがすべてを受け取る必要はなくて 起動直後の内容だけでいいです
つまりは job.sh が終われば close イベントが起きてほしいです
しかし close イベントは起きません
子プロセスである job.sh は終了済みなのですが孫プロセスの job.js が生きていることで stdout や stderr が生きてるのでこれらが閉じられるまでは Node.js は子プロセスが終了したとみなしてくれないようです
放置でもいい気はしますがずっと残るわけなのでメモリリーク状態です
stdio が問題なので ignore にしてしまえば標準入出力を使わないようにできて これなら即終了とみなされ CLOSED が表示されます
ただし問題があって標準出力を受け取れないです
ファイルに出力するようにして ファイルを読み取るということはできますが 回りくどくてイマイチです
起動する側もサーバーみたいな常駐するものだとメモリリークになるのを気にしないとですけど 一回限りのスクリプトでスクリプトが終了しないのが嫌という場合は unref で解決できます
必要な出力が出る程度に 1 秒待ってから unref しています
ちゃんとやるなら stdout の出力から必要なものを受け取れたら実行みたいにしたほうがいいです
まぁ終了させるなら unref していかなくても process.exit でもいいのですけどね
考えてみると常駐してるものから出力を受け取り続ける以上終了しないのが正常ですよね
job.sh で job.js を起動する部分で
みたいにしてしまったほうがいいかもしれませんね
[job.sh]
#!/bin/bash
echo pre something something
./job.js &
echo post something something
[job.js]
#!/usr/bin/env node
console.log("Start job.js")
setTimeout(() => {
console.log("initialized")
}, 500)
// keep process
setInterval(() => {}, 100000)
job.js は常駐するプロセスです
ここでは setInterval でプロセスが終了しないようにしています
これを job.sh から起動します
job.sh は別の Node.js プロセスから起動します
job.sh を起動する側はこんな感じです
const cp = require("child_process")
const sub = cp.spawn("./job.sh")
sub.stdout.setEncoding("utf8")
sub.stdout.on("data", console.log)
sub.on("close", () => {
console.log("CLOSED")
})
job.sh や job.js の出力を受け取りたいので stdout の標準出力にリスナをつけています
job.js は常駐しますがすべてを受け取る必要はなくて 起動直後の内容だけでいいです
つまりは job.sh が終われば close イベントが起きてほしいです
しかし close イベントは起きません
子プロセスである job.sh は終了済みなのですが孫プロセスの job.js が生きていることで stdout や stderr が生きてるのでこれらが閉じられるまでは Node.js は子プロセスが終了したとみなしてくれないようです
放置でもいい気はしますがずっと残るわけなのでメモリリーク状態です
stdio が問題なので ignore にしてしまえば標準入出力を使わないようにできて これなら即終了とみなされ CLOSED が表示されます
const sub = cp.spawn("./job.sh", { stdio: "ignore" })
ただし問題があって標準出力を受け取れないです
ファイルに出力するようにして ファイルを読み取るということはできますが 回りくどくてイマイチです
起動する側もサーバーみたいな常駐するものだとメモリリークになるのを気にしないとですけど 一回限りのスクリプトでスクリプトが終了しないのが嫌という場合は unref で解決できます
const cp = require("child_process")
sub = cp.spawn("./job.sh")
sub.stdout.setEncoding("utf8")
sub.stdout.on("data", console.log)
sub.on("close", () => {
console.log("CLOSED")
})
setTimeout(() => {
sub.unref()
sub.stdout.unref()
sub.stderr.unref()
}, 1000)
必要な出力が出る程度に 1 秒待ってから unref しています
ちゃんとやるなら stdout の出力から必要なものを受け取れたら実行みたいにしたほうがいいです
まぁ終了させるなら unref していかなくても process.exit でもいいのですけどね
考えてみると常駐してるものから出力を受け取り続ける以上終了しないのが正常ですよね
job.sh で job.js を起動する部分で
./job.js > file 2>&1 &
みたいにしてしまったほうがいいかもしれませんね
TypeScript が使われてるところが相変わらず増えてるなぁと思いながら ふとサーバーサイドって何使ってるんだろうと思いました
やっぱり Node.js なのでしょうか
Deno は TypeScript をサポートしているので TypeScript ユーザーは積極的に移行して Deno が流行るのかなと思ったこともありましたけど 最近はあまり流行ってないようです
そもそも Deno という名前を目にする機会がほぼないです
Deno が npm をサポートしてしまってからはどんどん注目されなくなりつつあるとか聞いたこともあります
たしかに私自身 npm をサポートしたあたりであまり興味がなくなってきましたし
npm を捨てて完全に新しいものという点で興味があったのに 結局 npm を使うなら Node.js でいいやと言う感じです
ライブラリ作者視点でも npm に登録しておけば Node.js からも Deno からも使えるならとりあえず npm に登録となって ユーザーも npm で使えるなら Node.js のままという感じでしょうか
私の場合は TypeScript を使わないので Node.js のままでも全然問題ないですが TypeScript を使う人の場合は変換のひと手間が必要になるので 新規に TypeScript で何かを作るときは Deno にしてもいいと思うのですけどね
TypeScript の普及に比べて Deno があまり普及してないように思うので 何を使ってるのかなと疑問に思いました
ただ TypeScript/JavaScript の記事を思い出してもサーバーサイドとして使ってるのをあまり見ない気もします
みんな基本はフロントのみで Vite や Next.js みたいな既存ツールを使うためだけに使っていて サーバーサイドにはそもそも TypeScript/JavaScript を使ってないのかもしれません
そういう用途だけなら Node.js でパッケージを入れて実行するだけでいいですし
やっぱり Node.js なのでしょうか
Deno は TypeScript をサポートしているので TypeScript ユーザーは積極的に移行して Deno が流行るのかなと思ったこともありましたけど 最近はあまり流行ってないようです
そもそも Deno という名前を目にする機会がほぼないです
Deno が npm をサポートしてしまってからはどんどん注目されなくなりつつあるとか聞いたこともあります
たしかに私自身 npm をサポートしたあたりであまり興味がなくなってきましたし
npm を捨てて完全に新しいものという点で興味があったのに 結局 npm を使うなら Node.js でいいやと言う感じです
ライブラリ作者視点でも npm に登録しておけば Node.js からも Deno からも使えるならとりあえず npm に登録となって ユーザーも npm で使えるなら Node.js のままという感じでしょうか
私の場合は TypeScript を使わないので Node.js のままでも全然問題ないですが TypeScript を使う人の場合は変換のひと手間が必要になるので 新規に TypeScript で何かを作るときは Deno にしてもいいと思うのですけどね
TypeScript の普及に比べて Deno があまり普及してないように思うので 何を使ってるのかなと疑問に思いました
ただ TypeScript/JavaScript の記事を思い出してもサーバーサイドとして使ってるのをあまり見ない気もします
みんな基本はフロントのみで Vite や Next.js みたいな既存ツールを使うためだけに使っていて サーバーサイドにはそもそも TypeScript/JavaScript を使ってないのかもしれません
そういう用途だけなら Node.js でパッケージを入れて実行するだけでいいですし
process.getActiveResourcesInfo() を使うとイベントループに残ってる一覧を文字列で見れる
setTimeout を登録すると Timeout が追加された
setTimeout を複数登録すると 配列の中の Timeout が複数になる
残ってる非同期処理の数がわかるのでどれくらい負荷があるかの参考にできる
ファイル指定で実行すると最初は何も入ってなくて 2 回目からデフォルトのものが含まれる
unref すると active 扱いされなくなって一覧から消える
最初から入ってるものは OS や CJS/ESM でも違うみたい
これを実行すると
Windows の場合
Linux の場合
バージョンはどっちも Node.js 18
Windows だと TTYWrap が 2 つ入ってる
Linux だと 1 つ
REPL だと Windows も Linux も TTYWrap がもう一つ増える
ESM だと CloseReq が最初から入ってる
CJS だと入ってない
Welcome to Node.js v18.17.1.
Type ".help" for more information.
> console.log(process.getActiveResourcesInfo())
[ 'TTYWrap', 'TTYWrap', 'TTYWrap' ]
> setTimeout(() => {}, 10000)
> console.log(process.getActiveResourcesInfo())
[ 'TTYWrap', 'TTYWrap', 'TTYWrap', 'Timeout' ]
setTimeout を登録すると Timeout が追加された
setTimeout を複数登録すると 配列の中の Timeout が複数になる
残ってる非同期処理の数がわかるのでどれくらい負荷があるかの参考にできる
console.log(0, process.getActiveResourcesInfo())
console.log(1, process.getActiveResourcesInfo())
setTimeout(() => {}, 100)
console.log(2, process.getActiveResourcesInfo())
const timeout = setTimeout(() => {}, 100)
console.log(3, process.getActiveResourcesInfo())
timeout.unref()
console.log(4, process.getActiveResourcesInfo())
require("fs/promises").readFile(__filename)
console.log(5, process.getActiveResourcesInfo())
0 []
1 [ 'TTYWrap', 'TTYWrap' ]
2 [ 'TTYWrap', 'TTYWrap', 'Timeout' ]
3 [ 'TTYWrap', 'TTYWrap', 'Timeout', 'Timeout' ]
4 [ 'TTYWrap', 'TTYWrap', 'Timeout' ]
5 [ 'FSReqPromise', 'TTYWrap', 'TTYWrap', 'Timeout' ]
ファイル指定で実行すると最初は何も入ってなくて 2 回目からデフォルトのものが含まれる
unref すると active 扱いされなくなって一覧から消える
最初から入ってるものは OS や CJS/ESM でも違うみたい
console.log(0, process.getActiveResourcesInfo())
console.log(1, process.getActiveResourcesInfo())
これを実行すると
Windows の場合
D:\t12>node a.cjs
0 []
1 [ 'TTYWrap', 'TTYWrap' ]
D:\t12>node a.mjs
0 [ 'CloseReq' ]
1 [ 'CloseReq', 'TTYWrap', 'TTYWrap' ]
Linux の場合
root@426b4d5c3e42:/opt# node a.cjs
0 []
1 [ 'TTYWrap' ]
root@426b4d5c3e42:/opt# node a.mjs
0 [ 'CloseReq' ]
1 [ 'CloseReq', 'TTYWrap' ]
バージョンはどっちも Node.js 18
Windows だと TTYWrap が 2 つ入ってる
Linux だと 1 つ
REPL だと Windows も Linux も TTYWrap がもう一つ増える
ESM だと CloseReq が最初から入ってる
CJS だと入ってない
Node.js 20.4 で Explicit Resource Management の proposal をサポートしたそうです
試してみましたが using は構文エラーでした
リソースの解放が必要な Node.js の API で using に対応しただけで using そのものはまだ使えないみたいです
--experimental 系フラグを見てもそれらしいのはまだなかったです
using を使って解放可能なリソースのオブジェクトは Symbol.dispose や Symbol.asyncDispose プロパティを持っていて 解放処理の関数が入ってます
それらが追加されたということみたいです
対応前のバージョンだと
ですが 対応後のバージョンだと
ファイルを読み取る stream では Symbol.asyncDispose に関数が入ってますね
シンボルが nodejs.dispose という名前になってますが V8 エンジン側でまだサポートされてないから Node.js で独自に用意してるだけで 正式に V8 側で対応したら独自のものじゃなくなるのだと思います
試してみましたが using は構文エラーでした
リソースの解放が必要な Node.js の API で using に対応しただけで using そのものはまだ使えないみたいです
--experimental 系フラグを見てもそれらしいのはまだなかったです
using を使って解放可能なリソースのオブジェクトは Symbol.dispose や Symbol.asyncDispose プロパティを持っていて 解放処理の関数が入ってます
それらが追加されたということみたいです
対応前のバージョンだと
Welcome to Node.js v18.13.0.
Type ".help" for more information.
> Symbol.dispose
undefined
> Symbol.asyncDispose
undefined
ですが 対応後のバージョンだと
Welcome to Node.js v20.5.0.
Type ".help" for more information.
> Symbol.dispose
Symbol(nodejs.dispose)
> Symbol.asyncDispose
Symbol(nodejs.asyncDispose)
> fs.writeFileSync("a", "")
undefined
> const a = fs.createReadStream("a")
undefined
> a[Symbol.dispose]
undefined
> a[Symbol.asyncDispose]
[Function (anonymous)]
ファイルを読み取る stream では Symbol.asyncDispose に関数が入ってますね
シンボルが nodejs.dispose という名前になってますが V8 エンジン側でまだサポートされてないから Node.js で独自に用意してるだけで 正式に V8 側で対応したら独自のものじゃなくなるのだと思います
Node.js で stream を使うとき デフォルトだと Buffer として受け取ります
文字列が欲しいときは Buffer の toString で変換できます
そういうコードを結構見かけます
データを受け取るたび toString した文字列を結合していって最終的な文字列を作ってたりです
でもこれが安全なのは英語圏だけです
日本語などのマルチバイト文字のときに問題が起きます
たとえば日本語の「あ」「い」「う」は UTF-8 ではこういう 3 バイトで表現されます
あ: 227, 129, 130
い: 227, 129, 132
う: 227, 129, 134
stream では基本的に複数のチャンクに分割されています
分割される場所が 1 文字の途中だった場合はこうなります
不正なデータとなり 「い」 のそれぞれのバイトが 1 文字の�とされています
こういうことがあるので全部受け取ってから Buffer を結合後に文字列化したほうが安全です
でも streaming 的に処理したい場合は最後を待ってられないです
こういうときは encoding を utf-8 にしておくと 受け取るデータが最初から文字列になってるだけじゃなくてマルチバイト文字の切れ目もうまく扱ってくれます
const stream = require("stream")
const timer = require("timers/promises")
const readable = new stream.Readable({
read() {},
})
readable.on("data", chunk => {
console.log(chunk)
})
readable.on("end", () => {
console.log("END")
})
const push = async () => {
await timer.setTimeout(1000)
readable.push(Buffer.from("abc"))
await timer.setTimeout(1000)
readable.push(Buffer.from("def"))
await timer.setTimeout(1000)
readable.push(null)
}
push()
<Buffer 61 62 63>
<Buffer 64 65 66>
END
文字列が欲しいときは Buffer の toString で変換できます
そういうコードを結構見かけます
データを受け取るたび toString した文字列を結合していって最終的な文字列を作ってたりです
でもこれが安全なのは英語圏だけです
日本語などのマルチバイト文字のときに問題が起きます
たとえば日本語の「あ」「い」「う」は UTF-8 ではこういう 3 バイトで表現されます
あ: 227, 129, 130
い: 227, 129, 132
う: 227, 129, 134
stream では基本的に複数のチャンクに分割されています
分割される場所が 1 文字の途中だった場合はこうなります
const stream = require("stream")
const timer = require("timers/promises")
const readable = new stream.Readable({
read() {},
})
readable.on("data", chunk => {
console.log(chunk, chunk.toString())
})
readable.on("end", () => {
console.log("END")
})
const push = async () => {
await timer.setTimeout(1000)
readable.push(Buffer.from([227, 129, 130, 227])) // い の 1 バイト目まで
await timer.setTimeout(1000)
readable.push(Buffer.from([129, 132, 227, 129, 134])) // い の 2 バイト目から
await timer.setTimeout(1000)
readable.push(null)
}
push()
<Buffer e3 81 82 e3> あ�
<Buffer 81 84 e3 81 86> ��う
END
不正なデータとなり 「い」 のそれぞれのバイトが 1 文字の�とされています
こういうことがあるので全部受け取ってから Buffer を結合後に文字列化したほうが安全です
でも streaming 的に処理したい場合は最後を待ってられないです
こういうときは encoding を utf-8 にしておくと 受け取るデータが最初から文字列になってるだけじゃなくてマルチバイト文字の切れ目もうまく扱ってくれます
const stream = require("stream")
const timer = require("timers/promises")
const readable = new stream.Readable({
encoding: "utf-8", // ←追加
read() {},
})
readable.on("data", chunk => {
console.log(chunk)
})
readable.on("end", () => {
console.log("END")
})
const push = async () => {
await timer.setTimeout(1000)
readable.push(Buffer.from([227, 129, 130, 227])) // い の 1 バイト目まで
await timer.setTimeout(1000)
readable.push(Buffer.from([129, 132, 227, 129, 134])) // い の 2 バイト目から
await timer.setTimeout(1000)
readable.push(null)
}
push()
あ
いう
END
POST リクエストでペイロード部分をサーバー側でちゃんと受け取れてないケースがあって 原因調査のためにあれこれやってると再現したりしなかったり
余計なライブラリなしで 再現性がある程度あるものが用意できた と思ったら別原因で別の問題になってました
その時のコードがこんなの
[server.js]
[client.js]
fetch ではなく request を使っていたり 一旦ファイルに書き込んで readable stream を pipe しているのは 元の現象が発生したときに使っていたライブラリの内部実装に合わせたためです
出力は
[client]
[server]
分かりづらいですが 5 回分のリクエストがあります
1 回目は正常なのですが 2 回目以降はサーバー側に途中までしかデータが届いていません
DATA の行の 2 つめの数字がそのリクエストの合計バイト数なので 前の行より減っているところからが次のリクエストです
クライアント側ではレスポンスは受け取れていますが finish イベントが起きなくなっています
なんどかプロセスを再起動してみても最初だけが成功します
接続したままになってるのかと思って socket を見てみると 途中までの場合でも毎回ちゃんとクローズしてるようです
サーバー側のリクエストハンドラにこういうコードを追加します
原因不明で苦戦していたのですが すごく単純な理由でした
サーバー側のリクエストハンドラではペイロードを取得するためのイベントリスナを設定してはいますが レスポンスは同期的に即返しています
レスポンスを返してしまってるので もう残りのリクエストのペイロードを送信する必要はないと判断して途中で切断してしまっているみたいです
を req の end イベントのハンドラ内に移すと この現象は発生しなくなりました
余計なライブラリなしで 再現性がある程度あるものが用意できた と思ったら別原因で別の問題になってました
その時のコードがこんなの
[server.js]
require("http").createServer((req, res) => {
let size = 0
req.on("data", (chunk) => {
console.log("DATA", chunk.length, size += chunk.length)
})
req.on("end", () => {
console.log("END")
})
req.on("error", (err) => {
console.log("ERROR", err)
})
res.end("ok")
}).listen(8000)
[client.js]
const fs = require("fs")
const { request } = require("http")
const data = "abcd".repeat(1024 * 100)
fs.writeFileSync("tmp", data)
setInterval(() => {
const req = request({
hostname: "localhost",
port: 8000,
path: "/",
method: "POST",
headers: {
"Content-Type": "text/plain",
},
}, res => {
const buffers = []
res.on("data", (chunk) => {
buffers.push(chunk)
console.log("RES DATA", chunk.length)
})
res.on("end", () => {
const response = Buffer.concat(buffers).toString()
console.log("RES END", response)
})
})
req.on("finish", () => {
console.log("REQ FINISH")
})
req.on("error", (err) => {
console.log("REQ ERROR", err)
})
fs.createReadStream("tmp").pipe(req)
}, 1000 * 10)
fetch ではなく request を使っていたり 一旦ファイルに書き込んで readable stream を pipe しているのは 元の現象が発生したときに使っていたライブラリの内部実装に合わせたためです
出力は
[client]
> node .\client.js
REQ FINISH
RES DATA 2
RES END ok
RES DATA 2
RES END ok
RES DATA 2
RES END ok
RES DATA 2
RES END ok
RES DATA 2
RES END ok
[server]
> node .\server.js
DATA 65390 65390
DATA 146 65536
DATA 65381 130917
DATA 155 131072
DATA 65372 196444
DATA 164 196608
DATA 65363 261971
DATA 173 262144
DATA 65354 327498
DATA 182 327680
DATA 65345 393025
DATA 191 393216
DATA 16384 409600
END
DATA 65390 65390
DATA 146 65536
DATA 65381 130917
DATA 155 131072
DATA 65372 196444
DATA 164 196608
DATA 65390 65390
DATA 146 65536
DATA 65381 130917
DATA 155 131072
DATA 65372 196444
DATA 164 196608
DATA 65390 65390
DATA 146 65536
DATA 65381 130917
DATA 155 131072
DATA 65390 65390
DATA 146 65536
DATA 65381 130917
DATA 155 131072
DATA 65372 196444
DATA 164 196608
分かりづらいですが 5 回分のリクエストがあります
1 回目は正常なのですが 2 回目以降はサーバー側に途中までしかデータが届いていません
DATA の行の 2 つめの数字がそのリクエストの合計バイト数なので 前の行より減っているところからが次のリクエストです
クライアント側ではレスポンスは受け取れていますが finish イベントが起きなくなっています
なんどかプロセスを再起動してみても最初だけが成功します
接続したままになってるのかと思って socket を見てみると 途中までの場合でも毎回ちゃんとクローズしてるようです
サーバー側のリクエストハンドラにこういうコードを追加します
req.socket.on("close", () => {
console.log("SOCK CLOSE")
})
(略)
DATA 16384 409600
END
SOCK CLOSE
(略)
DATA 164 196608
SOCK CLOSE
(略)
DATA 164 196608
SOCK CLOSE
原因不明で苦戦していたのですが すごく単純な理由でした
サーバー側のリクエストハンドラではペイロードを取得するためのイベントリスナを設定してはいますが レスポンスは同期的に即返しています
レスポンスを返してしまってるので もう残りのリクエストのペイロードを送信する必要はないと判断して途中で切断してしまっているみたいです
res.end("ok")
を req の end イベントのハンドラ内に移すと この現象は発生しなくなりました
REPL でデータを確認したりフィルタしたり変換したりと色々したいことがあります
そこで使うデータは別のファイルを読み取って変換したデータになるのですが REPL の中で毎回その準備をするのは大変です
1 回の REPL プロセス内で必要なデータの確認まで全てやって終わりならともかく変数等をクリアしたくてプロセスを終了して再度実行はけっこうあります
あれを見たいと思うたびに REPL のプロセスを起動してデータを読み取って準備しては手間です
「↑」キーで履歴呼び出しができるのですが 1 実行ごとの呼び出しなので初回準備が何行にも及ぶと再実行が面倒です
スクリプトにまとめておいてそれを require するだけにすれば 1 行になりますが REPL 操作部分が長くなると履歴からその require を探すのも一苦労だったりします
ということでいい方法を探したところ -r オプションでモジュールをロードするのが良さそうでした
仮にデータは 1 行 1 JSON 形式で保存されたテキストファイルで それをパースしてオブジェクトの配列として扱うとします
この pre.js を -r で読み込むと REPL が実行される前にロードされるのでグローバル変数にデータを入れておけば参照できます
データのファイルが複数あり ファイルを選択できるようにしたい場合はコマンドライン引数で渡すようにもできます
プロセスを終了して再実行するときには node コマンド内の REPL で実行したコマンドは関係なくシェル側の履歴から参照するので 1 回 「↑」 を押して再実行するだけです
そこで使うデータは別のファイルを読み取って変換したデータになるのですが REPL の中で毎回その準備をするのは大変です
1 回の REPL プロセス内で必要なデータの確認まで全てやって終わりならともかく変数等をクリアしたくてプロセスを終了して再度実行はけっこうあります
あれを見たいと思うたびに REPL のプロセスを起動してデータを読み取って準備しては手間です
「↑」キーで履歴呼び出しができるのですが 1 実行ごとの呼び出しなので初回準備が何行にも及ぶと再実行が面倒です
スクリプトにまとめておいてそれを require するだけにすれば 1 行になりますが REPL 操作部分が長くなると履歴からその require を探すのも一苦労だったりします
ということでいい方法を探したところ -r オプションでモジュールをロードするのが良さそうでした
仮にデータは 1 行 1 JSON 形式で保存されたテキストファイルで それをパースしてオブジェクトの配列として扱うとします
> cat data.txt
{"x":1,"y":2}
{"x":4,"y":0}
{"x":2,"y":10}
> cat pre.js
globalThis.items = require("fs").readFileSync("./data.txt").toString()
.split("\n").filter(x => x.trim()).map(x => JSON.parse(x))
この pre.js を -r で読み込むと REPL が実行される前にロードされるのでグローバル変数にデータを入れておけば参照できます
> node -r "./pre.js"
Welcome to Node.js v18.12.1.
Type ".help" for more information.
> items.filter(item => item.x === 1)
[ { x: 1, y: 2 } ]
データのファイルが複数あり ファイルを選択できるようにしたい場合はコマンドライン引数で渡すようにもできます
> cat pre.js
globalThis.items = require("fs").readFileSync(process.argv[2]).toString()
.split("\n").filter(x => x.trim()).map(x => JSON.parse(x))
> node -r "./pre.js" - data.txt
Welcome to Node.js v18.12.1.
Type ".help" for more information.
> items
[ { x: 1, y: 2 }, { x: 4, y: 0 }, { x: 2, y: 10 } ]
プロセスを終了して再実行するときには node コマンド内の REPL で実行したコマンドは関係なくシェル側の履歴から参照するので 1 回 「↑」 を押して再実行するだけです
(1) foo.js を読み込んで foo.js が foo/ 以下のモジュールを読み込む
(2) foo/index.js を読み込んで foo/index.js が foo/ 以下のモジュールを読み込む
CJS なら (2) が相性がいい
フォルダ名の foo を require すれば自動で index.js を補完してくれる
もとは foo.js 単体であとからモジュール分けするときに foo で require していれば読み込む側のコードは変更しなくていい
それに foo に関係するモジュールはすべて foo フォルダに入ってる
だけど ESM は CJS みたいな補完はなくて完全なファイル名が必要
どのモジュールをロードしているのかわかりやすくはあるけど import に foo/index.js のように書かないといけなくなる
foo.js からモジュール分けすると使うところの変更も必要
だから ESM なら (1) 形式にしたほうがいいのかなと最近思ってたりする
foo.js が foo/ を単純にインポートしてエクスポートするだけならまだいいけどここでも処理を入れると foo フォルダに入っていて欲しくなる
foo.js
foo/file1.js
foo/file2.js
(2) foo/index.js を読み込んで foo/index.js が foo/ 以下のモジュールを読み込む
foo/index.js
foo/file1.js
foo/file2.js
CJS なら (2) が相性がいい
フォルダ名の foo を require すれば自動で index.js を補完してくれる
もとは foo.js 単体であとからモジュール分けするときに foo で require していれば読み込む側のコードは変更しなくていい
それに foo に関係するモジュールはすべて foo フォルダに入ってる
だけど ESM は CJS みたいな補完はなくて完全なファイル名が必要
どのモジュールをロードしているのかわかりやすくはあるけど import に foo/index.js のように書かないといけなくなる
foo.js からモジュール分けすると使うところの変更も必要
だから ESM なら (1) 形式にしたほうがいいのかなと最近思ってたりする
foo.js が foo/ を単純にインポートしてエクスポートするだけならまだいいけどここでも処理を入れると foo フォルダに入っていて欲しくなる
最近では ESM 版しか用意せず CJS 版がないパッケージもでてきているようです
CJS からでも import() を使えば ESM モジュールを読み込めますが 非同期処理になります
CJS ではトップレベルの await ができないので ESM のみのパッケージが入るだけであちこちで非同期処理が発生してとても使いづらいです
中にはこれまで CJS にしていたのにアップデートで ESM のみになるパッケージもあります
あるパッケージの最新の機能を使いたいのでアップデートしたら とても困ったことになりました
パッケージの読み込み等はすべて同期処理を前提として作っているものだったので まともに対応するなら大幅な作り直しが必要になります
まともに対応なんてやってられないので別の方法で回避することにしました
一つの方法は CJS 化すること
これまで CJS だったのですから こっちで CJS に変換してから使います
Rollup などを使います
ただこれには問題があって 対象が依存パッケージにも及びます
「MyPackage → A → B」 のような依存関係になっているとき A が ESM のみだと B も ESM のみの可能性があります
一つのプロジェクトでパッケージを分割してるような場合だと A の依存関係にも ESM のみパッケージが含まれるケースが多いです
自分のパッケージからの直接の依存関係である A だけを CJS 化しても A から B をインポートするときに require だとインポートできないというエラーになります
ESM パッケージすべての変換が必要ですが 多くなってくると 1 つ 1 つを CJS 化するのは大変です
手抜きをするなら A の CJS 変換時に依存関係も解決してバンドルすることです
A の変換済み CJS モジュールには A も B も含みます
楽にはなりますが B のパッケージが複数の A からインポートされる場合に A ごとに異なる B が存在することになります
同じパッケージを別パッケージとして複数回読み込むことになるのでムダが多いです
さらにキャッシュなどで状態を持つモジュールがあると 別モジュールとみなされることで動作に影響する場合もあります
class なら instanceof を使った判定を行うケースもあり 別モジュールだと実体が違うので別物とみなされ意図した動作にならない場合もあります
この点ではパッケージ 1 つ 1 つを CJS 化した方が良いです
別の方法では CJS 化せずそのまま使います
問題は import() によって読み込みが非同期化することなので 事前にすべての読み込みを終えてからメインの処理を実行することで非同期読み込みを不要にします
エントリポイントのスクリプトが index.js なら boot.js を追加してエントリポイントをこっちに変更します
boot.js はこんな感じにします
esm_modules という空のモジュールを用意して ここを import 済み ESM モジュール置き場にします
ESM のパッケージをすべて ここで import() します
読み込みが終われば esm_modules のプロパティに保持します
これらの処理が終わってから require で本来のエントリポイントの index.js を実行します
その他モジュールでは ESM のみパッケージを require するところを esm_modules の require に置き換えます
index.js 以降の処理では ESM のみパッケージは変数に保持されているので非同期処理が不要になります
多少のコードの修正は必要ですが 非同期対応に比べれば遥かに簡単な修正で済みます
CJS からでも import() を使えば ESM モジュールを読み込めますが 非同期処理になります
CJS ではトップレベルの await ができないので ESM のみのパッケージが入るだけであちこちで非同期処理が発生してとても使いづらいです
中にはこれまで CJS にしていたのにアップデートで ESM のみになるパッケージもあります
あるパッケージの最新の機能を使いたいのでアップデートしたら とても困ったことになりました
パッケージの読み込み等はすべて同期処理を前提として作っているものだったので まともに対応するなら大幅な作り直しが必要になります
まともに対応なんてやってられないので別の方法で回避することにしました
一つの方法は CJS 化すること
これまで CJS だったのですから こっちで CJS に変換してから使います
Rollup などを使います
ただこれには問題があって 対象が依存パッケージにも及びます
「MyPackage → A → B」 のような依存関係になっているとき A が ESM のみだと B も ESM のみの可能性があります
一つのプロジェクトでパッケージを分割してるような場合だと A の依存関係にも ESM のみパッケージが含まれるケースが多いです
自分のパッケージからの直接の依存関係である A だけを CJS 化しても A から B をインポートするときに require だとインポートできないというエラーになります
ESM パッケージすべての変換が必要ですが 多くなってくると 1 つ 1 つを CJS 化するのは大変です
手抜きをするなら A の CJS 変換時に依存関係も解決してバンドルすることです
A の変換済み CJS モジュールには A も B も含みます
楽にはなりますが B のパッケージが複数の A からインポートされる場合に A ごとに異なる B が存在することになります
同じパッケージを別パッケージとして複数回読み込むことになるのでムダが多いです
さらにキャッシュなどで状態を持つモジュールがあると 別モジュールとみなされることで動作に影響する場合もあります
class なら instanceof を使った判定を行うケースもあり 別モジュールだと実体が違うので別物とみなされ意図した動作にならない場合もあります
この点ではパッケージ 1 つ 1 つを CJS 化した方が良いです
別の方法では CJS 化せずそのまま使います
問題は import() によって読み込みが非同期化することなので 事前にすべての読み込みを終えてからメインの処理を実行することで非同期読み込みを不要にします
エントリポイントのスクリプトが index.js なら boot.js を追加してエントリポイントをこっちに変更します
boot.js はこんな感じにします
const esm_modules = require("./esm_modules.js")
!async function() {
const modules = [
"mod1",
"mod2",
// ...
]
const loaded = await Promise.all(modules.map(name => import(name)))
for (const [idx, name] of modules.entries()) {
esm_modules[name] = loaded[idx]
}
require("./index.js")
}()
esm_modules という空のモジュールを用意して ここを import 済み ESM モジュール置き場にします
ESM のパッケージをすべて ここで import() します
読み込みが終われば esm_modules のプロパティに保持します
これらの処理が終わってから require で本来のエントリポイントの index.js を実行します
その他モジュールでは ESM のみパッケージを require するところを esm_modules の require に置き換えます
const { mod1 } = require("./esm_modules.js")
index.js 以降の処理では ESM のみパッケージは変数に保持されているので非同期処理が不要になります
多少のコードの修正は必要ですが 非同期対応に比べれば遥かに簡単な修正で済みます
Node.js 18 で最初にスナップショット機能が入ると聞いたときに調べたら Node.js のビルド時のもので その方法でビルドされた Node.js の実行ファイルはスナップショットの状態から起動するというものでした
そんな気軽に使えないものなら使わないかなと思っていましたが もっと簡単に使えるスナップショット機能も入ったようです
というスクリプトを用意して --build-snapshot で指定します
すると snapshot.blob ファイルが作られるので これを --snapshot-blob に指定して実行します
ここでは実行するスクリプトを指定していないので REPL が起動しています
ここで foo と bar にアクセスしてみると bar の方は 1 が入ってます
グローバル変数以外のモジュールのトップレベルのスコープなどは引き継がないみたいで foo の方は見つかりませんでした
export の代わりに globalThis に入れておけば Node.js でスクリプトの実行時に最初から使えます
準備に時間がかかる処理があって スクリプトを繰り返し実行するときには便利そうですね
コマンドラインツールはもちろんですが ウェブサーバーやアプリケーションでも呼び出すと起動する関数をグローバル変数に配置しておいて 実行時のスクリプトはそれを呼び出すだけにすると高速で起動ができそうです
ただグローバル変数を使うのはあまりやりたくないです
モジュールにすればできるかなと思ったのですが require してもあるはずのモジュールが見つかりませんでした
require を見てみると
というもので通常の require とは別物になってるようです
基準パスが変わってたりするのかなと 絶対パスでモジュールを指定してみましたが これでもインポートできませんでした
調べてみるとドキュメントに記載がありました
https://nodejs.org/docs/latest-v18.x/api/cli.html#--build-snapshot
まだ 実験的なものなのでいくつか制限があって そのひとつでユーザーランドのモジュールはサポートしていないようです
バンドルして 1 ファイルにすれば動かせるそうです
ということはグローバル変数に置くしかなさそうですね
それにブラウザでならともかく Node.js でまでバンドルはしたくないので この辺がサポートされるの待ちです
完全に使えるようになると色々便利そうなので期待の機能です
そんな気軽に使えないものなら使わないかなと思っていましたが もっと簡単に使えるスナップショット機能も入ったようです
const foo = 1
globalThis.bar = foo
というスクリプトを用意して --build-snapshot で指定します
node --build-snapshot ss.js
すると snapshot.blob ファイルが作られるので これを --snapshot-blob に指定して実行します
root@d4572737f3a3:/tmp# node --snapshot-blob snapshot.blob
Welcome to Node.js v18.10.0.
Type ".help" for more information.
> foo
Uncaught ReferenceError: foo is not defined
> bar
1
ここでは実行するスクリプトを指定していないので REPL が起動しています
ここで foo と bar にアクセスしてみると bar の方は 1 が入ってます
グローバル変数以外のモジュールのトップレベルのスコープなどは引き継がないみたいで foo の方は見つかりませんでした
export の代わりに globalThis に入れておけば Node.js でスクリプトの実行時に最初から使えます
準備に時間がかかる処理があって スクリプトを繰り返し実行するときには便利そうですね
コマンドラインツールはもちろんですが ウェブサーバーやアプリケーションでも呼び出すと起動する関数をグローバル変数に配置しておいて 実行時のスクリプトはそれを呼び出すだけにすると高速で起動ができそうです
ただグローバル変数を使うのはあまりやりたくないです
モジュールにすればできるかなと思ったのですが require してもあるはずのモジュールが見つかりませんでした
require を見てみると
[Function: requireForUserSnapshot]
というもので通常の require とは別物になってるようです
基準パスが変わってたりするのかなと 絶対パスでモジュールを指定してみましたが これでもインポートできませんでした
調べてみるとドキュメントに記載がありました
https://nodejs.org/docs/latest-v18.x/api/cli.html#--build-snapshot
まだ 実験的なものなのでいくつか制限があって そのひとつでユーザーランドのモジュールはサポートしていないようです
バンドルして 1 ファイルにすれば動かせるそうです
ということはグローバル変数に置くしかなさそうですね
それにブラウザでならともかく Node.js でまでバンドルはしたくないので この辺がサポートされるの待ちです
完全に使えるようになると色々便利そうなので期待の機能です
ビルトインの readline モジュールはユーザーからの入力受付用かと思っていましたが ファイルの読み取りにも使えるみたいです
readline で作成した Interface は async iterator を持ってるので for-await-of で使えます
Node.js 17 から readline に Promise API が追加されています
これは question みたいな関数がコールバックか Promise かの違いで 行ごとに読み取るだけなら 16 まででも使えます
18 以降でも変わりないです
注意しないといけないのがこういうケースです
readline の Interface を作ってから for-await-of で読むまでに非同期処理を挟む場合です
これを実行すると 1 だけが出力されます
for-await-of では待機したままデータなしになって 解決しない Promise という扱いで それ以降実行できるものがなくプロセスが終了します
その結果 2 を出力する console.log にたどり着かないので出力は 1 だけです
内部で stream の resume が呼び出されるので自動で読み進めてしまうみたいです
非同期処理を挟まず for-await-of を実行すると asyncIterator の作成処理で stream のイベントが起きる前にリスナを設定できます
その結果 正常に line イベントなどを受け取れて期待どおりに動作します
しかし 非同期処理を挟むと先にイベントが起きてしまって stream が close されたあとにリスナをつけることになるのでなんのイベントも起きず解決されない Promise になるということみたいです
const fs = require("fs")
const readline = require("readline")
const fn = async () => {
fs.writeFileSync("test.txt", "foo\nbar\nbaz")
const rl = readline.createInterface({ input: fs.createReadStream("test.txt") })
for await (const line of rl) {
console.log({ line })
}
}
fn()
{ line: 'foo' }
{ line: 'bar' }
{ line: 'baz' }
readline で作成した Interface は async iterator を持ってるので for-await-of で使えます
const readline = require("readline")
const rl = readline.createInterface({
input: { on(){}, resume() {} }
})
console.log(rl[Symbol.iterator])
// undefined
console.log(rl[Symbol.asyncIterator])
// [Function (anonymous)]
Node.js 17 から readline に Promise API が追加されています
これは question みたいな関数がコールバックか Promise かの違いで 行ごとに読み取るだけなら 16 まででも使えます
18 以降でも変わりないです
注意しないといけないのがこういうケースです
const fs = require("fs")
const readline = require("readline")
const fn = async () => {
fs.writeFileSync("test.txt", "foo\nbar\nbaz")
const rl = readline.createInterface({ input: fs.createReadStream("test.txt") })
await new Promise(r => setTimeout(r, 100))
console.log(1)
for await (const line of rl) {
console.log({ line })
}
console.log(2)
}
fn()
readline の Interface を作ってから for-await-of で読むまでに非同期処理を挟む場合です
これを実行すると 1 だけが出力されます
for-await-of では待機したままデータなしになって 解決しない Promise という扱いで それ以降実行できるものがなくプロセスが終了します
その結果 2 を出力する console.log にたどり着かないので出力は 1 だけです
内部で stream の resume が呼び出されるので自動で読み進めてしまうみたいです
非同期処理を挟まず for-await-of を実行すると asyncIterator の作成処理で stream のイベントが起きる前にリスナを設定できます
その結果 正常に line イベントなどを受け取れて期待どおりに動作します
しかし 非同期処理を挟むと先にイベントが起きてしまって stream が close されたあとにリスナをつけることになるのでなんのイベントも起きず解決されない Promise になるということみたいです
JSON を POST リクエストするちょっとしたスクリプトが必要なとき Node.js はあまり使いませんでした
標準の http モジュールだと辛すぎで node-fetch などなにかのパッケージが必要になります
ちょっとしたスクリプトなのに npm のパッケージを入れるのはなんかなーと思うところがあります
zx も同様
ただ他にいい方法もあまりないんですよね
単純なリクエストなら curl ですが JSON を送るとなると面倒です
jq コマンドを使って複雑なことをしてる例を見ましたが これなら他の言語でいいかなと思います
結局ベストと言える方法もなかったのですが Node.js が 18 で標準で fetch が使えるようになりました
トップレベル await もあるのですごくシンプルにかけます
で
を送信できます
ただし トップレベル await は CJS だと使えません
ESM とするには package.json に type を書く必要があります
package.json も作りたくないところでは .mjs 拡張子にする必要がありこれが微妙です
いっそ deno という手もありますが 追加インストールしないといけないのがネックです
コンテナを使うならどっちでもいいですが それなら zx にしてしまう方がいいのかもとも思ったりです
標準の http モジュールだと辛すぎで node-fetch などなにかのパッケージが必要になります
ちょっとしたスクリプトなのに npm のパッケージを入れるのはなんかなーと思うところがあります
zx も同様
ただ他にいい方法もあまりないんですよね
単純なリクエストなら curl ですが JSON を送るとなると面倒です
jq コマンドを使って複雑なことをしてる例を見ましたが これなら他の言語でいいかなと思います
結局ベストと言える方法もなかったのですが Node.js が 18 で標準で fetch が使えるようになりました
トップレベル await もあるのですごくシンプルにかけます
const [param1, param2] = process.argv.slice(2)
const data = { param1, param2 }
const res = await fetch("url", { headers: { "content-type": "application/json" }, body: JSON.stringify(data) })
const result = await res.json()
console.log(res.status, result)
node <ファイル名> foo bar
で
{ "param1": "foo", "param2": "bar" }
を送信できます
ただし トップレベル await は CJS だと使えません
ESM とするには package.json に type を書く必要があります
package.json も作りたくないところでは .mjs 拡張子にする必要がありこれが微妙です
いっそ deno という手もありますが 追加インストールしないといけないのがネックです
コンテナを使うならどっちでもいいですが それなら zx にしてしまう方がいいのかもとも思ったりです
https://nodejs.org/en/blog/announcements/nodejs16-eol/
もともと Node.js 16 で OpenSSL 3 に切り替える予定だったけど間に合わなくて 1.1.1 になってて このバージョンの OpenSSL の EOL は 2023/09/11 らしい
Node.js 16 の EOL はもっと先の予定だけど EOL を迎えたバージョンの OpenSSL を使い続けるのは脆弱性リスクがあるし OpenSSL 3 にすると互換性の問題もあるしということで Node.js 16 の EOL を OpenSSL 1.1.1 に合わせて早めることにしたみたい
本来の予定だと
https://nodejs.org/en/about/releases/
LTS バージョンごとの Active LTS 開始 / Maintenance LTS 開始 / EOL 日付は
いつも通りだけど毎年 10 月末に新しい LTS がリリースされる
約 1 年経つとメンテナンス LTS に切り替わる
それから約 1 年半後の 4 月末で EOL
Node.js 16 は 2024/04/30 が 2023/09/11 に変わるということみたい
14 の EOL から半年もないので結構短め
最近までは毎年の LTS が変わるたびに大きめな新機能が入ったりで毎年更新してたけど 16 からはそこまで大きく変わらないし しばらくは 16 でいいかななんて思ってたけど 今年も早めに 18 に切り替えて行く方がいいのかも
もともと Node.js 16 で OpenSSL 3 に切り替える予定だったけど間に合わなくて 1.1.1 になってて このバージョンの OpenSSL の EOL は 2023/09/11 らしい
Node.js 16 の EOL はもっと先の予定だけど EOL を迎えたバージョンの OpenSSL を使い続けるのは脆弱性リスクがあるし OpenSSL 3 にすると互換性の問題もあるしということで Node.js 16 の EOL を OpenSSL 1.1.1 に合わせて早めることにしたみたい
本来の予定だと
https://nodejs.org/en/about/releases/
LTS バージョンごとの Active LTS 開始 / Maintenance LTS 開始 / EOL 日付は
v14 2020-10-27 2021-10-19 2023-04-30
v16 2021-10-26 2022-10-18 2024-04-30
v18 2022-10-25 2023-10-18 2025-04-30
いつも通りだけど毎年 10 月末に新しい LTS がリリースされる
約 1 年経つとメンテナンス LTS に切り替わる
それから約 1 年半後の 4 月末で EOL
Node.js 16 は 2024/04/30 が 2023/09/11 に変わるということみたい
14 の EOL から半年もないので結構短め
最近までは毎年の LTS が変わるたびに大きめな新機能が入ったりで毎年更新してたけど 16 からはそこまで大きく変わらないし しばらくは 16 でいいかななんて思ってたけど 今年も早めに 18 に切り替えて行く方がいいのかも
Node.js 18 の新機能でスナップショット機能が入ると聞いて期待してたのですが思ってたのと違いました
思ってたのは好きなタイミングでメモリ状態をファイルに書き込むことができて Node.js の起動時にそのファイルを指定すれば同じ状態から起動できるというもの
サーバみたいに常駐するものならともかく コマンドラインで繰り返し実行する系のツールでは node_modules のモジュールを毎回ロードするので遅いですしムダに感じます
最初からモジュールがロード済みの状態で起動できて ユーザが入力したコマンドに応じた処理だけをすれば無駄なくいい感じになりそうです
ですが実際にはそんな便利なものではなく Node.js をビルドするときに .js ファイルを指定できるというもの
指定してビルドされた Node.js 実行ファイルを実行すると その .js ファイルを実行した状態でファイルを実行できるようです
自分でビルドしないといけないので結構面倒で気軽に使うものではなさそうです
また 実行時にスナップショットファイルを指定できるわけではないので スナップショットの数だけ Node.js の実行ファイルが作られます
ライブラリやツールごとに Node.js 実行ファイルがあるようなものです
すでに Electron はそういう状態でアプリごとに Electron 本体が含まれているので Electron アプリを色々入れるとそれだけでストレージが結構圧迫されます
そんななので自分で使う機会は特になさそうです
いくつかのツールはこれに移ってインストール方法が変わるかもしれませんね
思ってたのは好きなタイミングでメモリ状態をファイルに書き込むことができて Node.js の起動時にそのファイルを指定すれば同じ状態から起動できるというもの
サーバみたいに常駐するものならともかく コマンドラインで繰り返し実行する系のツールでは node_modules のモジュールを毎回ロードするので遅いですしムダに感じます
最初からモジュールがロード済みの状態で起動できて ユーザが入力したコマンドに応じた処理だけをすれば無駄なくいい感じになりそうです
ですが実際にはそんな便利なものではなく Node.js をビルドするときに .js ファイルを指定できるというもの
指定してビルドされた Node.js 実行ファイルを実行すると その .js ファイルを実行した状態でファイルを実行できるようです
自分でビルドしないといけないので結構面倒で気軽に使うものではなさそうです
また 実行時にスナップショットファイルを指定できるわけではないので スナップショットの数だけ Node.js の実行ファイルが作られます
ライブラリやツールごとに Node.js 実行ファイルがあるようなものです
すでに Electron はそういう状態でアプリごとに Electron 本体が含まれているので Electron アプリを色々入れるとそれだけでストレージが結構圧迫されます
そんななので自分で使う機会は特になさそうです
いくつかのツールはこれに移ってインストール方法が変わるかもしれませんね
何度も書いてる try-catch の扱いづらさ
ブロックスコープな上に try が文なせいで色々面倒になる
Node.js の Sync 系関数の場合 Promise を返す版にすれば結構楽になる
Web サーバとかじゃないスクリプトで並行に処理する必要もないときは Sync にしてたけど これを考えると Sync にしないほうが楽
例
エラーを見るなら
ブロックスコープな上に try が文なせいで色々面倒になる
Node.js の Sync 系関数の場合 Promise を返す版にすれば結構楽になる
Web サーバとかじゃないスクリプトで並行に処理する必要もないときは Sync にしてたけど これを考えると Sync にしないほうが楽
例
// Sync の場合
let stat
try {
stat = fs.statSync(dir)
if (!stat.isDirectory()) {
return
}
} catch (err) {
return
}
// Promise の場合
const stat = await fs.promises.stat(dir).catch(err => null)
if (!stat || !stat.isDirectory()) {
return
}
エラーを見るなら
const stat = await fs.promises.stat(dir).catch(err => err)
if (stat instanceof Error) {
//
}
if (!stat.isDirectory()) {
return
}
久々にみたら 1.0 になってた
https://github.com/knex/knex/blob/master/CHANGELOG.md
0.21 だったり 0.95 だったりでずっと 1 を超えなくて 0.X 系バージョニングを採用してるものとして取り上げられてたりもしたけど とうとう 1.0
大きく変わったことを期待したけど変化はそれほどなさそう
0.95 のときのほうが大きかった気がする
そういえば forever のときも大したことなかった気がするし 0.X 系プロジェクトの 1.0 にそこまで大きな意味はないのかも
影響しそうな破壊的変更はこれ
「Changed data structure from RETURNING operation to be consistent with SELECT」
以前書いたこともある returning を使った時の返り値の仕様
select メソッドだと .select("col1", "col2") のように可変長引数
returning は第 2 引数がオプションなので列名の指定は .returning(["col1", "col2"]) のように第 1 引数で配列形式
配列じゃなくて文字列単体で .returning("col1") とも書ける
この場合は ひとつだけなので返り値の配列の中の要素がオブジェクトではなく列の値になってる
慣れないと分かりづらいところはあるけど 引数の型が違うし 一つしか無いのにオブジェクトにする必要もなく合理的と思ってた
returning に id だけを指定してから id の配列にするために
というのはよく必要になるわけでこの手間をなくせる
だけど文字列でも "*" みたいなのが来ればオブジェクトになるし 気をつけないといけないポイントみたいになってた
それが 1.0 では変更されて常にオブジェクトになるようになった
id だけ指定して id の配列を受け取るようにしてる人は割といるんじゃないかと思うし 影響の大きな変更といえるかも
mysql/mariadb はこの機能がそもそもなかったと思うから影響受けるのは PostgreSQL/Oracle/MSSQL ユーザだけのはず
https://github.com/knex/knex/blob/master/CHANGELOG.md
0.21 だったり 0.95 だったりでずっと 1 を超えなくて 0.X 系バージョニングを採用してるものとして取り上げられてたりもしたけど とうとう 1.0
大きく変わったことを期待したけど変化はそれほどなさそう
0.95 のときのほうが大きかった気がする
そういえば forever のときも大したことなかった気がするし 0.X 系プロジェクトの 1.0 にそこまで大きな意味はないのかも
影響しそうな破壊的変更はこれ
「Changed data structure from RETURNING operation to be consistent with SELECT」
以前書いたこともある returning を使った時の返り値の仕様
select メソッドだと .select("col1", "col2") のように可変長引数
returning は第 2 引数がオプションなので列名の指定は .returning(["col1", "col2"]) のように第 1 引数で配列形式
配列じゃなくて文字列単体で .returning("col1") とも書ける
この場合は ひとつだけなので返り値の配列の中の要素がオブジェクトではなく列の値になってる
await pg("table").insert([{ value: "a" }, { value: "b" }]).returning(["id"])
// [{ id: 1 }, { id: 2 }]
await pg("table").insert([{ value: "a" }, { value: "b" }]).returning("id")
// [1, 2]
慣れないと分かりづらいところはあるけど 引数の型が違うし 一つしか無いのにオブジェクトにする必要もなく合理的と思ってた
returning に id だけを指定してから id の配列にするために
const ids = rows.map(row => row.id)
というのはよく必要になるわけでこの手間をなくせる
だけど文字列でも "*" みたいなのが来ればオブジェクトになるし 気をつけないといけないポイントみたいになってた
それが 1.0 では変更されて常にオブジェクトになるようになった
await pg("table").insert([{ value: "a" }, { value: "b" }]).returning(["id"])
// [{ id: 1 }, { id: 2 }]
await pg("table").insert([{ value: "a" }, { value: "b" }]).returning("id")
// [{ id: 1 }, { id: 2 }]
id だけ指定して id の配列を受け取るようにしてる人は割といるんじゃないかと思うし 影響の大きな変更といえるかも
mysql/mariadb はこの機能がそもそもなかったと思うから影響受けるのは PostgreSQL/Oracle/MSSQL ユーザだけのはず
https://blog.flatt.tech/entry/node_mysql_sqlinjection
話題になってた Node.js の mysql ライブラリで SQL インジェクションができるというやつ
Node.js と mysql の組み合わせは考えてみると使ったことがなくて どのパッケージがいいかもわかってないくらい
いつか使うかもしれないし 危険があるなら知っておくためにも読んでみるかと開いてみました
長いから全部は読まず最初の概要だけしか読んでないけど プレースホルダーのパラメータでオブジェクトを渡すと予期せぬ結果になって SQL インジェクションできるらしい
え?これって使う人が悪いだけじゃないの?
外部から受け取ったデータをチェックもせずに DB のクエリに入れるなんてありえないし
メールアドレスや郵便番号のフォーマットに沿ってるかみたいなところまでは見なくても 最低限型のチェックくらいはするのが当たり前だと思う
Node.js やこのライブラリが良くないみたいな反応も見かけたけど 使い方が悪いだけで問題として扱うようなものでもない気がする
チュートリアルの説明ではバリデーションがないけど そのライブラリの使い方として重要なところでもないからそういうのはスキップして書かれるのは普通だし 最低限の例をそのまま正式なところで使うようなのはありえないと思う
趣味レベルで外部に公開しないようなものならともかく 世界にサービスとして公開するような人がそんなレベルだとは思いたくない
それに プレースホルダーがどんな文字列になろうと SQL インジェクションにならない気がするけど それがありえるってことは mysql 側のプリペアドステートメント機能を使わずライブラリ側でエミュレートして文字列化して送信してるのかな
そういうのは DB 側に任せたほうが安全な気がするけど
最初は特定の文字列だと正しくエスケープされないバグみたいのがあるのかなとか思ってましたが 特に気にするほどでも無いものでした
ユーザの入力は信用しないという当たり前のことをしようという話ですね
話題になってた Node.js の mysql ライブラリで SQL インジェクションができるというやつ
Node.js と mysql の組み合わせは考えてみると使ったことがなくて どのパッケージがいいかもわかってないくらい
いつか使うかもしれないし 危険があるなら知っておくためにも読んでみるかと開いてみました
長いから全部は読まず最初の概要だけしか読んでないけど プレースホルダーのパラメータでオブジェクトを渡すと予期せぬ結果になって SQL インジェクションできるらしい
え?これって使う人が悪いだけじゃないの?
外部から受け取ったデータをチェックもせずに DB のクエリに入れるなんてありえないし
メールアドレスや郵便番号のフォーマットに沿ってるかみたいなところまでは見なくても 最低限型のチェックくらいはするのが当たり前だと思う
Node.js やこのライブラリが良くないみたいな反応も見かけたけど 使い方が悪いだけで問題として扱うようなものでもない気がする
チュートリアルの説明ではバリデーションがないけど そのライブラリの使い方として重要なところでもないからそういうのはスキップして書かれるのは普通だし 最低限の例をそのまま正式なところで使うようなのはありえないと思う
趣味レベルで外部に公開しないようなものならともかく 世界にサービスとして公開するような人がそんなレベルだとは思いたくない
それに プレースホルダーがどんな文字列になろうと SQL インジェクションにならない気がするけど それがありえるってことは mysql 側のプリペアドステートメント機能を使わずライブラリ側でエミュレートして文字列化して送信してるのかな
そういうのは DB 側に任せたほうが安全な気がするけど
最初は特定の文字列だと正しくエスケープされないバグみたいのがあるのかなとか思ってましたが 特に気にするほどでも無いものでした
ユーザの入力は信用しないという当たり前のことをしようという話ですね
前に SQL のクエリでわざと少し遅くしようとしたことがあり 遅くなるものといえば正規表現かなと試してみましたが 想像以上に速かったです
普通の正規表現だと遅いと言ってもそんなに変わらないので 遅くなるような正規表現を探したところシンプルなのだとこういうのが見つかりました
まずは Node.js でどれくらいかかるのか調べると
が 17 秒ほど
最適化されるのか 3 回目くらいから 1 秒程度になりましたけど
が 9.8 秒ほど
repeat で 10 万文字もありますが 「.+a」 ではなく 「a.+」 だと 1 ミリ秒未満なので 文字数が多いせいというわけではないです
これを PostgreSQL で試したのですが
速すぎです
数ミリ秒程度です
遅い正規表現はエンジンの実装次第なところがあるらしいですが パフォーマンスには力を入れているはずの V8 でも遅いものをこれだけの速度で実行できるというのはすごいです
ユーザ入力で遅くなりうる正規表現を実行するなら SQL として PostgreSQL 側で実行するのもありかも?
普通の正規表現だと遅いと言ってもそんなに変わらないので 遅くなるような正規表現を探したところシンプルなのだとこういうのが見つかりました
^(\d+)*$
.+a
まずは Node.js でどれくらいかかるのか調べると
/^(\d+)*$/.test("1234567890123456789012345678a")
が 17 秒ほど
最適化されるのか 3 回目くらいから 1 秒程度になりましたけど
/.+a/.test("1".repeat(100000))
が 9.8 秒ほど
repeat で 10 万文字もありますが 「.+a」 ではなく 「a.+」 だと 1 ミリ秒未満なので 文字数が多いせいというわけではないです
これを PostgreSQL で試したのですが
postgres=# \timing
Timing is on.
postgres=# select '1234567890123456789012345678a' ~ '^(\d+)*$';
?column?
----------
f
(1 row)
Time: 2.068 ms
postgres=# select '1234567890123456789012345678a' ~ '^(\d+)*$';
?column?
----------
f
(1 row)
Time: 0.538 ms
postgres=# select '1234567890123456789012345678a' ~ '^(\d+)*$';
?column?
----------
f
(1 row)
Time: 0.675 ms
postgres=# \timing
Timing is on.
postgres=# select repeat('1', 100000) ~ '.+a';
?column?
----------
f
(1 row)
Time: 5.444 ms
postgres=# select repeat('1', 100000) ~ '.+a';
?column?
----------
f
(1 row)
Time: 5.588 ms
postgres=# select repeat('1', 100000) ~ '.+a';
?column?
----------
f
(1 row)
Time: 5.080 ms
速すぎです
数ミリ秒程度です
遅い正規表現はエンジンの実装次第なところがあるらしいですが パフォーマンスには力を入れているはずの V8 でも遅いものをこれだけの速度で実行できるというのはすごいです
ユーザ入力で遅くなりうる正規表現を実行するなら SQL として PostgreSQL 側で実行するのもありかも?
https://github.com/request/request
1 年以上前の 2020/2/11 に完全に deprecated になったみたい
request はかなり昔からある古いもので重いから普段は使わないけど 最近の軽量系だと対応してないことをしたいときには使うことがあったし 使ってるライブラリが内部的に使ってたりもして このライブラリをインストールしてるプロジェクトはいくつかある
基本は node-fetch でいいんだけど ブラウザ互換だから足りない機能もあるし そういう場合に使うのは何にするか悩むところ
代替についての Issue は作られてる
https://github.com/request/request/issues/3143
axios は React の流行り始めのころにフロントエンドで使われてたらしいけど fetch のない IE サポート用だし 最近は積極的にメンテナンスされず放置気味らしいし ブラウザでも動くものを探してるわけじゃないので除外
superagent は昔からあるけどこれ使うなら request でいいってものだったから これも違う
その他はあまり聞かないのが多いし 機能やダウンロード数などを考えると got かな
1 年以上前の 2020/2/11 に完全に deprecated になったみたい
request はかなり昔からある古いもので重いから普段は使わないけど 最近の軽量系だと対応してないことをしたいときには使うことがあったし 使ってるライブラリが内部的に使ってたりもして このライブラリをインストールしてるプロジェクトはいくつかある
基本は node-fetch でいいんだけど ブラウザ互換だから足りない機能もあるし そういう場合に使うのは何にするか悩むところ
代替についての Issue は作られてる
https://github.com/request/request/issues/3143
axios は React の流行り始めのころにフロントエンドで使われてたらしいけど fetch のない IE サポート用だし 最近は積極的にメンテナンスされず放置気味らしいし ブラウザでも動くものを探してるわけじゃないので除外
superagent は昔からあるけどこれ使うなら request でいいってものだったから これも違う
その他はあまり聞かないのが多いし 機能やダウンロード数などを考えると got かな
現在時刻を設定したいけど アプリケーションサーバのタイムスタンプじゃなくてデータベースサーバのタイムスタンプに統一したいところ
Node.js で knex を使っていて knex だと現在時刻を設定したいところは pg.fn.now() を入れておけば良い
そのとき update するためのオブジェクトを作るところは別モジュールで pg を参照できなくて とりあえず "now" という文字列で渡しておいて update を実行するところで置換すればいいやと思ってた
その後 pg.fn.now に置き換える処理を追加するのを忘れてたけどなぜか普通に動いてる
調べてみると "now" という文字列を日付型に保存しようとすると自動で現在時刻として扱われるみたい
knex 固有の機能というわけではなくて psql で SQL を書くときでも一緒
単にキャストすればそうなる
すごく便利
pg.fn.now() なんて長いの書く必要なかった
Node.js で knex を使っていて knex だと現在時刻を設定したいところは pg.fn.now() を入れておけば良い
そのとき update するためのオブジェクトを作るところは別モジュールで pg を参照できなくて とりあえず "now" という文字列で渡しておいて update を実行するところで置換すればいいやと思ってた
その後 pg.fn.now に置き換える処理を追加するのを忘れてたけどなぜか普通に動いてる
調べてみると "now" という文字列を日付型に保存しようとすると自動で現在時刻として扱われるみたい
knex 固有の機能というわけではなくて psql で SQL を書くときでも一緒
単にキャストすればそうなる
postgres=# select 'now'::timestamp;
timestamp
----------------------------
2021-09-18 06:33:46.890335
(1 row)
すごく便利
pg.fn.now() なんて長いの書く必要なかった
リクエストは API のみで HTML のあとに CSS とか JavaScript とかをロードすることはないサーバ
同じクライアントからのリクエスト頻度はたまにあるくらい
keep-alive のメリットが特にないし keep-alive することで前書いたようなサーバを即時止めるために少し手間が必要になる
これは keep-alive を無効でいいかと思ったけどドキュメントを見ても無効化する方法が載ってない
server.keepAliveTimeout というのがあるけどこれはタイムアウトの設定だから 0 にしてもタイムアウトしなくなるだけ
たぶんクライアントが切断するまでつないだままになる
調べると レスポンスの HTTP ヘッダで 「Connection: close」 を送ればいいというのがあったけど 直接ヘッダ操作はあまりしたくない
ソースコードを見てると shouldKeepAlive というプロパティがあって これを見てヘッダの Connection を追加してる
https://github.com/nodejs/node/blob/v14.17.6/lib/_http_outgoing.js#L436
HTTP 1.0 だと false になってて HTTP バージョンによって自動で判断されてる
このプロパティを false にすれば無効にできそう
これで試してみるとレスポンスは 「Connection: close」 になって ちゃんと無効化できてた
同じクライアントからのリクエスト頻度はたまにあるくらい
keep-alive のメリットが特にないし keep-alive することで前書いたようなサーバを即時止めるために少し手間が必要になる
これは keep-alive を無効でいいかと思ったけどドキュメントを見ても無効化する方法が載ってない
server.keepAliveTimeout というのがあるけどこれはタイムアウトの設定だから 0 にしてもタイムアウトしなくなるだけ
たぶんクライアントが切断するまでつないだままになる
調べると レスポンスの HTTP ヘッダで 「Connection: close」 を送ればいいというのがあったけど 直接ヘッダ操作はあまりしたくない
ソースコードを見てると shouldKeepAlive というプロパティがあって これを見てヘッダの Connection を追加してる
https://github.com/nodejs/node/blob/v14.17.6/lib/_http_outgoing.js#L436
HTTP 1.0 だと false になってて HTTP バージョンによって自動で判断されてる
このプロパティを false にすれば無効にできそう
http.createServer((req, res) => {
res.shouldKeepAlive = false
res.end("ok")
})
これで試してみるとレスポンスは 「Connection: close」 になって ちゃんと無効化できてた
RDB のクエリで WHERE にタイムスタンプ型の列を対象にするとなぜか毎回何も一致しません
レコードが存在するのは確認していて SELECT で取得したデータをそのまま WHERE に入れても一致しません
これに結構苦戦させられたのですが 単純に精度の問題でした
環境は Node.js なのでタイムスタンプ型は Date 型として取得できます
node-pg でもそれをラップした knex 等でも同じです
Date 型はミリ秒までですが PostgreSQL で timestamp と指定したときの精度はマイクロ秒まであります
なので Node.js で受け取った段階でマイクロ秒は切り捨てられて その値で検索しても一致はしないわけです
大きな数値の ID などで数値型でも有り得そうな問題ですが 受け取ったタイムスタンプが見た目上問題なさそうなので原因特定に時間がかかりました
言語の都合でデータベースの定義を変更するのはどうかなという気はするものの その Node.js のアプリ専用なデータベースですし 実際にはミリ秒以下は必要になることはないので秒までにすることで対処しました
タイムスタンプ型のオプションで精度は変更できます
また CURRENT_TIMESTAMP などを使わず Node.js 側で現在時刻を取得して保存すればマイクロ秒は常に 0 になるので定義を変更せずに対処することもできます
もしマイクロ秒まで必要となると select 時にキャストして文字列型で取得するしかなさそうです
レコードが存在するのは確認していて SELECT で取得したデータをそのまま WHERE に入れても一致しません
これに結構苦戦させられたのですが 単純に精度の問題でした
環境は Node.js なのでタイムスタンプ型は Date 型として取得できます
node-pg でもそれをラップした knex 等でも同じです
Date 型はミリ秒までですが PostgreSQL で timestamp と指定したときの精度はマイクロ秒まであります
なので Node.js で受け取った段階でマイクロ秒は切り捨てられて その値で検索しても一致はしないわけです
大きな数値の ID などで数値型でも有り得そうな問題ですが 受け取ったタイムスタンプが見た目上問題なさそうなので原因特定に時間がかかりました
言語の都合でデータベースの定義を変更するのはどうかなという気はするものの その Node.js のアプリ専用なデータベースですし 実際にはミリ秒以下は必要になることはないので秒までにすることで対処しました
タイムスタンプ型のオプションで精度は変更できます
また CURRENT_TIMESTAMP などを使わず Node.js 側で現在時刻を取得して保存すればマイクロ秒は常に 0 になるので定義を変更せずに対処することもできます
もしマイクロ秒まで必要となると select 時にキャストして文字列型で取得するしかなさそうです
pg("table")
.select("id", pg.raw("xxx_timestamp::text"))
なんとなく Node.js のソースコードを見てると deps に acorn がありました
https://github.com/nodejs/node/tree/master/deps
どこに使ってるんだろうと探すと assert と repl のようです
追加されたタイミングは 10.0 でした
https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V10.md#10.0.0
PR: https://github.com/nodejs/node/pull/15566
Commit: https://github.com/nodejs/node/commit/eeab7bc068
Node.js の REPL ってトップレベル await 対応してたんですね
せっかくなので試してみると……
async function の中でしか使えないというエラーです
導入されたのは結構前なのに未だにフラグが必要みたいです
https://github.com/nodejs/node/tree/master/deps
どこに使ってるんだろうと探すと assert と repl のようです
追加されたタイミングは 10.0 でした
https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V10.md#10.0.0
PR: https://github.com/nodejs/node/pull/15566
Commit: https://github.com/nodejs/node/commit/eeab7bc068
Node.js の REPL ってトップレベル await 対応してたんですね
せっかくなので試してみると……
C:\>node
Welcome to Node.js v14.15.4.
Type ".help" for more information.
> await 1
await 1
^^^^^
Uncaught SyntaxError: await is only valid in async function
async function の中でしか使えないというエラーです
導入されたのは結構前なのに未だにフラグが必要みたいです
C:\>node --experimental-repl-await
Welcome to Node.js v14.15.4.
Type ".help" for more information.
> await 1
1
Python だと GUI を作るライブラリで tkinter とか kivy とか聞くけど Node.js って NW.js や Electron くらいしか聞かない
ブラウザ組み込み系じゃなく GUI を作るライブラリってないのかなと調べてみた
● Node-Qt
http://documentup.com/arturadib/node-qt (Github)
C++ Qt bindings
この手のライブラリはラップしてるだけだと思うので C++ で Qt 使ったことある人には向いてるかも
2014 年から更新されてない
bindings だし安定してるのか放置されてるのか
● node-gtk
(Github)
GNOME Gtk+ bindings
こっちは最終更新は 2020 年末で最近
Windows だと使えなさそう
● NodeGui
https://docs.nodegui.org/ (Github)
一番人気そうな GUI ライブラリ
内部は Qt 使ってるらしい
React や Vue にも対応してるらしいけど そういうの使うなら Electron でいい気がする
React Native は Windows 版もあった気がするし
● Yue
https://libyue.com/ (Github)
リポジトリ内に C++, Lua, Node.js がまとめて入っていてドキュメントも同じサイトにまとまってる
Qt などの移植と違って元プロジェクト自体が 3 言語サポートしてるみたい
既存のライブラリに求めてるのがなかったから自作したらしい
内部的に何使ってるかまでわかってない
base フォルダを見るに一部 Chromium のコードを流用してる?
Windows 版は GDI+ で将来的に Direct2D に置き換え予定だとか
ブラウザ組み込み系じゃなく GUI を作るライブラリってないのかなと調べてみた
● Node-Qt
http://documentup.com/arturadib/node-qt (Github)
C++ Qt bindings
この手のライブラリはラップしてるだけだと思うので C++ で Qt 使ったことある人には向いてるかも
2014 年から更新されてない
bindings だし安定してるのか放置されてるのか
● node-gtk
(Github)
GNOME Gtk+ bindings
こっちは最終更新は 2020 年末で最近
Windows だと使えなさそう
● NodeGui
https://docs.nodegui.org/ (Github)
一番人気そうな GUI ライブラリ
内部は Qt 使ってるらしい
React や Vue にも対応してるらしいけど そういうの使うなら Electron でいい気がする
React Native は Windows 版もあった気がするし
● Yue
https://libyue.com/ (Github)
リポジトリ内に C++, Lua, Node.js がまとめて入っていてドキュメントも同じサイトにまとまってる
Qt などの移植と違って元プロジェクト自体が 3 言語サポートしてるみたい
既存のライブラリに求めてるのがなかったから自作したらしい
内部的に何使ってるかまでわかってない
base フォルダを見るに一部 Chromium のコードを流用してる?
Windows 版は GDI+ で将来的に Direct2D に置き換え予定だとか
比較用に生の Node.js といくつかの Node.js フレームワークでログイン処理を作ってみたもの
https://gitlab.com/nexpr/nodejs-login
◯ Node.js
◯ koa
◯ api
◯ fastify
の 4 種類
パスが /secret/ の内側のみ 通常のルートも静的ファイルもログインが必要
外側なら誰でも見える
/ が各ページへのリンクがあるインデックスページ
ログインは id/pw を POST して cookie に保存するスタンダードなもの
ログインできるユーザ情報は user1/PW で固定
それぞれにログイン周りが少し違う 2 パターンを用意 (1.js と 2.js)
違いはログインしてない状態でログイン必要がページを開いたときの動き
1.js では /secret/login にログイン画面を用意
ここにリダイレクトされて ログインに成功したら元のアクセスしたページにリダイレクト
2.js ではリダイレクトせずアクセスしたページの URL でログイン画面が出る
https://gitlab.com/nexpr/nodejs-login
◯ Node.js
◯ koa
◯ api
◯ fastify
の 4 種類
パスが /secret/ の内側のみ 通常のルートも静的ファイルもログインが必要
外側なら誰でも見える
/ が各ページへのリンクがあるインデックスページ
ログインは id/pw を POST して cookie に保存するスタンダードなもの
ログインできるユーザ情報は user1/PW で固定
それぞれにログイン周りが少し違う 2 パターンを用意 (1.js と 2.js)
違いはログインしてない状態でログイン必要がページを開いたときの動き
1.js では /secret/login にログイン画面を用意
ここにリダイレクトされて ログインに成功したら元のアクセスしたページにリダイレクト
2.js ではリダイレクトせずアクセスしたページの URL でログイン画面が出る
https://github.com/byrro/misc-public-files/blob/master/public/ExWNT-white-bg.png
あまり使わないから r,w,a 以外忘れてる
+ があると r+,w+,a+ 全部読み書きできるから 書き込み位置と今のファイルを消すかの違い
Node.js だとこれにまだ追加があって
w,a,w+,a+ に x 付きの wx,ax,wx+,ax+ がある
x があると ファイルが存在する場合はエラー
r+,a,a+ に s 付きの rs+,as,as+ がある
rs はないみたい
s 付きは synchronous mode
Node.js の Sync とは違って IO 処理の同期モードだから Sync 系関数にしないと JavaScript の処理は完了を待たずに次の処理に進む
ディスクへの書き込みが同期処理だから通常より書き込み完了は遅くなるはず
ドキュメントでもパフォーマンスに影響するから必要ない限り推奨しないって書かれてる
あまり使わないから r,w,a 以外忘れてる
+ があると r+,w+,a+ 全部読み書きできるから 書き込み位置と今のファイルを消すかの違い
Node.js だとこれにまだ追加があって
w,a,w+,a+ に x 付きの wx,ax,wx+,ax+ がある
x があると ファイルが存在する場合はエラー
r+,a,a+ に s 付きの rs+,as,as+ がある
rs はないみたい
s 付きは synchronous mode
Node.js の Sync とは違って IO 処理の同期モードだから Sync 系関数にしないと JavaScript の処理は完了を待たずに次の処理に進む
ディスクへの書き込みが同期処理だから通常より書き込み完了は遅くなるはず
ドキュメントでもパフォーマンスに影響するから必要ない限り推奨しないって書かれてる
なんかすごいプロジェクトを見つけました
Lua ですが Node.js と同じように使えます
https://luvit.io/
トップページの例のコードなんて Node.js そのままです
ドキュメントの感じまで Node.js です
https://luvit.io/api/
内部で libuv を使っているので API が一緒なだけじゃなく 非同期処理も Node.js と同じ感じで動くはずです
また npm にあたるパッケージマネージャの lit というのも用意されています
https://github.com/luvit/lit
Lua ですが Node.js と同じように使えます
https://luvit.io/
トップページの例のコードなんて Node.js そのままです
local http = require('http')
http.createServer(function (req, res)
local body = "Hello world\n"
res:setHeader("Content-Type", "text/plain")
res:setHeader("Content-Length", #body)
res:finish(body)
end):listen(1337, '127.0.0.1')
print('Server running at http://127.0.0.1:1337/')
ドキュメントの感じまで Node.js です
https://luvit.io/api/
内部で libuv を使っているので API が一緒なだけじゃなく 非同期処理も Node.js と同じ感じで動くはずです
また npm にあたるパッケージマネージャの lit というのも用意されています
https://github.com/luvit/lit
書き込みを待たずに次の処理に行きたかったのでこんなのを書いたら
順番がバラバラだった
非同期とは言え書き込み順は保証されてると思ったのにそんなことなかった
キューに入れて書き込み順は保証するようにした
const fs = require("fs")
const w = text => {
fs.appendFile("./log.txt", text + "\n", () => {})
}
w("A")
w("B")
w("C")
w("D")
w("E")
w("F")
w("G")
w("H")
w("I")
w("J")
順番がバラバラだった
D
A
C
E
F
H
B
I
G
J
非同期とは言え書き込み順は保証されてると思ったのにそんなことなかった
キューに入れて書き込み順は保証するようにした
const fs = require("fs")
const w = (() => {
const q = []
let running = false
const run = async () => {
running = true
let text
while(text = q.shift()) {
await fs.promises.appendFile("./log.txt", text + "\n")
}
running = false
}
return text => {
q.push(text)
if (!running) run()
}
})()
w("A")
w("B")
w("C")
w("D")
w("E")
w("F")
w("G")
w("H")
w("I")
w("J")