Node.js や deno に Web Standard な API をなんでも取り入れるのが良いことなのかについて

この記事は Node.js Advent Calendar の 11 日目の記事です。

qiita.com

Web API と Node.js

ES2015 以前の Node.js は Web Standard な API の中で足りないものを自分で補う形で進化を続けてきた。 Callback や Event 主体での非同期処理や Common JS な形でロードできる独自のモジュールの仕組みがその筆頭だと思う。ただ逆に Web Standard な API が流行ると今度はそれに追従していかないといけなくなってきた。 ES2015 以後に流行ったものといえば、 Promise 主体での非同期処理であり、 async-await での処理だと思う。また、 ES Modules の台頭もあり、今日では Node.js でも普通に呼び出すことが可能になった。

今ではどちらも Node.js で普通に使える。エコシステムを壊さないようにした結果、 Node.js の ES Modules が普通に使えるようになるには時間がかかったが、いずれにせよ今は使えている。

TC39 だけが Web Standard なグループではない。 WHATWG や WICG 、 W3C などのグループもそれぞれ存在し、それぞれが Web Standard な API を作っている。これらを後追いで Node.js は使えるようにしてきた。 Event Target API, Text Encode / Decode, WHATWG URL, Web Stream, Web Crypto, AbortController などなど、足りないパーツを補う形で作られている。

deno は最初から Web Standard な API をベースに設計されており、割と Node.js よりも既存ブラウザに存在する機能を積極的に持ってきている方だと言える。後発なだけあって、エコシステムに配慮する必要がない分迅速に対応ができている。

Node.js / deno が Web Standard API に追従する状況は現在でも続いている。ただし、最近は若干やりすぎなのではないかというか、本当に必要なのか?と思うようなものまで入っているし、検討されている気がする。

自分の立場を明確にしておくと、「新しい Web API に追従することは良いことだと思うが、不要な API にまで追従する必要はないし、無理矢理ブラウザと同じ API にする必要もない」という立場だ。

新しい Web API が必要か不要かにはいくつかの観点があると思う。筆者は以下のように観点を感じている。

  • 全てのブラウザでコンセンサスが取れていること
  • Node.js / deno の利用者が呼び出した時にどうなるのかが既存の API と比較してイメージしやすいこと
  • 新しい機能が入ることでセキュリティホールが生まれにくいこと

この観点でいくつか考えてみようと思う。

atob / btoa が Node.js / deno に入った。

atob と btoa は 文字列を encode して base64 にしたり、 decode して元に戻す時に使う、binary から ascii (base64) に変換 (btoa) し、 ascii (base64) から binary に戻せる (atob) という API だ。ブラウザでは昔から実装されている。

developer.mozilla.org

ただし、この文字列は名前の通り ascii (base64) にだけしか適用できない。 btoa が binary to ascii (base64) の略語だと知っていれば binary (latin1) な文字列にしか使えないことはわかるが、一方で日本人のように latin1 以外の表現を文字列としてナチュラルに使っているところもあると思う。特に任意の文字列を base64 に変換する API だと誤解して使っていると不用意なバグを埋め込む可能性もある。 encodeURIComponent などで無理矢理日本語を binary (latin1) に変換してから使えば一応使えるが、イディオム的で直感的ではない。

> atob(btoa("こんにちは"))

Uncaught:
DOMException [InvalidCharacterError]: Invalid character

> decodeURIComponent(atob(btoa(encodeURIComponent("こんにちは"))))

'こんにちは'

すでに Node.js にも deno にも実装されている。しかし Node.js は実装した瞬間にこれは使うべきではないと実装者から言われている。

Node.js には Buffer API があるので、これを使わなくても、base64 に変換するような処理は表現することは可能。

const str = "こんにちは";
const base64 = Buffer.from(str, "utf-8").toString("base64");
console.log(Buffer.from(base64, "base64").toString("utf-8"));

こっちのほうが長いので、一見難しそうに見えるかもしれないが、やっていることは simple で utf-8 から base64 に変換している処理であることは掴みやすい。 btoa のような4文字で表現されているAPIは easy な API ではあるものの、ぱっと見てこれが binary to ascii の略で base64 に変換してくれる API だと調べないで分かる人は少ないのではないかと思う。

上述した観点でいうと、「既存のAPIと比較してイメージしにくい」という点と「知らないで使ってしまった時に不用意なバグを埋め込むのではないか」という点でそもそも Node.js には入れなくても良かった API だと思っている

じゃあそもそもなんで使ってほしくない API を Node.js が実装したのか、というと、Web Standard API に合わせるというコアチーム全体の合意ともう一つが atob / btoa を polyfill して作られているライブラリの存在、最後に deno が既に実装しているという競合からの後押しの3つの理由で実装しているのではないかと推測している。

自分の立場で言えば入れなくても良かった API だと思っているものの、多少複雑な思いもある。 Buffer が Node.js に詳しい人は分かっていたとしても、ブラウザ側のフロントエンドエンジニアにとっては atobbtoa の方が身近な存在である可能性はある。 Node.js のユーザーがそちらに傾きつつある現状においてはその方が良いという意見もわからなくはない。一方でブラウザの歴史的なレガシー API をそのまま持ってくることが本当に良いことなのかは慎重に検討したほうが良い気がする。 特に簡単(Easy) な API というのは難しい。誰かにとっては簡単でも、誰かにとっては不便なものだからだ。今回の例で言えば、「atob/btoaを知っているブラウザのフロントエンドエンジニアにとっては簡単」だけど、知らないエンジニアにとっては一見わかりにくい Bad Parts 的なものであり、これ以上増えていくことは避けるべきではないかと思っている。

だからこそ、コアチームの中にも矛盾した思いがあり、「新しく実装したけどなるべく使わないでくれ」というメッセージを出している。

File system access API

Web を構成する要素として一番難しいブロックの一つにファイルの取り扱いがある。この APIファイルシステムにアクセスできる API をブラウザに実装しようというものだ。

wicg.github.io

Node.js はまだ検討中で、実装するようなフェーズに入っていない。とはいえ、アイデアとしては検討はしているようだ。

github.com

deno は検討中で、 draft PR は出されている。

github.com

github.com

この API はブラウザ間のコンセンサスがまだ取れていない。

mozilla.github.io

ブラウザ間のコンセンサスが取れてない状況で実装したとしても変わる可能性は大いに有り得るし、最終的に実装されなかった場合には誰も得しない API になってしまう。 まだマージするフェーズにどちらも入っていないものの、Web Standard API を採用するとしても、ブラウザのコンセンサスはさすがに取られたものにしてほしい。プラットフォーム側がいち早く Web API を実装しなくとも、コミュニティ側が「使いたい」という意見が出てから実装したとしても遅くないように思う。

そもそもファイルを取り扱うという一番サーバサイドでよくありそうな基本的な処理をクライアントとして使われるブラウザの API に任せるのは難しい気がしている。

fetch

Node.js では未だに議論を重ねている fetch のサポートだが、 deno には既に入っている。ただ fetch もよくよく仕様を読むと deno / Node.js には不要なものも多い。特に CORS 周りの同じドメイン以外にリクエストを送る時の仕様は Cross Origin という概念が存在しないサーバーサイドでは形だけ API として設定できるように使われていて、実行しても何も意味がなかったりする。つまり、 mode: "same-origin" など設定できるものの特に無意味でリクエストは送れてしまう。こういう形だけの API はどこまで正確に模倣するべきなのかは議論が分かれるところだと思う。逆に Cross Origin 相当の設計を今から deno / Node.js に取り入れるのも労力の割にリターンが見合わないし、どういうものになるのか想像がつかない。

Cache をどうやって取り扱うのかも fetch 内にオプションとして設定できる。ブラウザであればブラウザの cache storage を使う際のオプションとして使われるが、 サーバーサイドで fetch した時にはもちろん無視される。というより、サーバサイドで統一された cache storage なんてものはないし、あったとしても実装がメモリ内に保存するのか永続化するのか、するんだとしてどうやって expired データを取り扱うのかといった概念をセキュリティに配慮しながら実装するのも不毛な気がしている。

Cookie とかはさらに頭が痛い問題である。ブラウザで幅広く使われているが、サーバサイドに持ってくるべきかどうかに関しては今もってお互い議論中だ。

なので、 fetch はあくまでも表向きのよく使われそうな仕様だけ実装してあり、ブラウザとの 100% compatible なものを目指すことは deno にせよ Node.js にせよ考えていない。

とはいえ、それでも HTTP をリクエストするという API においては、ブラウザにせよ Node/deno にせよ必要な API であり、どちらも表向きでいいから同じ API が欲しいというのは理解できる。ただし表向き同じ API というのがどこまでを指していっているのかが、Node.js / deno コミュニティ内で深く考えきれていない気がする。単純に似たようなものであれば、 Next.js でも提供されているし、 unfetch などの 3rd party 製のものもある。 Node.js のコアチームは undici と呼ばれる HTTP クライアントを次の HTTP クライアントとして提供している。

※ ちなみにマニアックな話になるが、 deno の fetch は ALPN を使った HTTP/2, HTTP/1.1 のネゴシエーションをしてくれるが、上述した Node.js のライブラリはどれも ALPN でのネゴシエーションはしてくれない。

fetch の中身を見ずにただ「fetchという表向き同じAPI」を指して、 fetch をコアの中に入れようとするとその「表向き同じ実装をどこまで頑張るのか」のコンセンサスを取るのに難しいし、既存のエコシステムを壊さないように入れるのは非常に時間がかかる。

筆者は fetch が一番複雑な思いを抱いている。現状の Node.js の http クライアントはブラウザのクライアントとはノリが違いすぎるので、気軽に call できる新しいクライアントはほしい。一方でブラウザがブラウザのために作った API と同じ API が実装されることは表向き良いとは思うものの、実際使ってみたら無意味な設定や設定しているつもりでも動かない機能が多くなり、結果として Bad Parts になってしまわないかという懸念はある。特に fetch は前述の simple か easy かという議論で言うと、 easy 寄りの API として提案されているように思える。 URL を fetch 関数に渡せば Promise で response が返ってくるという仕様は非常にわかりやすいが、実際には fetch クライアントが中でやっている処理は非常に複雑になっている。ブラウザが実装するものとしてはセキュリティに配慮した形でこのような API になることも理解できる。一方で、サーバサイドで呼び出す時にこの API がマッチしているのかに関しては、まだそこまで検討が進んでいないと思う。

自分が isomorphic だとか universal だとか言ってきた頃より時代が進み、同様なものが実装されるようになってきた。これ自体は良い兆候であると思う。一方でどこまで行っても「表向き同じもの」であって、細部がどこまで表現されているかはドキュメントには書かれていないことが多い。どこまで実装されているのかはもう少しドキュメントに書かれてほしい。

その他

これ以外にも clipboard API とかを実装したり、 navigator にあるようなクライアントの情報が取れる API を実装したりしようとしている issue も見かけたが、サーバサイドで実行された時にセキュリティホールになりそうだと最初に思ってしまった。基本的にブラウザのセキュリティモデルとサーバサイドで動くことが基本のプラットフォームとは同じ感覚で考えすぎるのは良くないと思っている。 deno にはパーミッションで防げる仕組みがあるとはいえ、サーバサイド内で実行されて困るような API を実装するべきではないと思っている。

まとめ

これまでは同じ API が増えることが Web というエコシステムを後押しするように思えていた。一方で、なんでもやりすぎるのはどうなのかと一旦立ち止まって考えるようになってしまった。特に atob/btoa を実装した辺りが個人的に立ち止まって考える切っ掛けになった部分だ。 Web Platform Test のカバレッジが増えることが良いことのようにされ、 mdn 上にある星取表が Yes になることが良いことだと思われているが、一方で、本当になんでも入れるのが良いことなのかについては考えていくようにして、フィードバックしたい。