HN 日本語サマリー

← 一覧へ戻る
プログラミング

Zigにおける型安全なHTMLテンプレート

Type-safe HTML templating in Zig (lalinsky.com)

27 pointsby lukaslalinsky7 コメント

要約

この記事では、Zig言語で型安全かつ高速なHTMLテンプレートエンジン「zt」の作成について解説しています。ztは、コンパイル時に型チェックを行い、XSS攻撃を防ぐための安全なデフォルト設定を備えています。

全文翻訳

長年にわたり、私は多くのWeb開発を行ってきました。2000年頃、PHP 3.0がリリースされWebの世界を席巻し始めた直後に始めました。生のPHP、Pythonでの文字列連結、PHPのSmartyテンプレート、PerlのTemplate Toolkit、PythonのJinja/Djangoテンプレートなど、あらゆる種類のHTML生成アプローチを使用してきました。その後、世界がSPAやJavaScriptフレームワークへと移行するにつれて、フロントエンドの作業から離れました。2年前にZigを手に取り、このような低レベル言語でWeb開発を検討することになるとは想像もしていませんでした。しかし、zioやDustyで独自のイコシステムを構築するにつれて、それがそれほど突飛なことではないと気づきました。APIバックエンドのためにこれらのライブラリを構築しましたが、その出力はJSONやMessagePackでした。しかし、アリーナアロケータを適切に使用すれば、通常関与するすべての文字列の解放を覚えておく必要がないため、Web開発でさえ実際には管理可能であることがわかりました。そしてZigのcomptimeを使用すると、パフォーマンスの利点を失うことなく非常に高レベルなインターフェースを構築できます。また、htmxやDatastarの助けを借りれば、JavaScript/TypeScriptに触れることなくSPAを構築することさえできます。これは、決してやらないと自分に誓ったことです。それでも、まだ1つ足りないものがありました。テンプレートです。文字列連結は選択肢になりませんでした。2000年以前のCGI時代に戻りたくありませんでした。テンプレートライブラリにはいくつかの要件がありました。すべての入力が型付けされ、コンパイル時にチェックされる、型安全である必要があります。私は人生で十分な動的言語を扱ってきたので、もう実行時テンプレートエラーを処理したくありません。高速である必要があります。アロケーションなし、中間状態なし、出力は直接std.Io.Writerに書き込むだけです。安全なデフォルト設定が必要です。XSSをもう処理したくないので、すべての入力はデフォルトでエスケープされる必要があります。それでもHTMLを書いているように感じる必要があります。私が戦わなければならないZigベースのDSLは欲しくありませんでした。これらの要件すべてを満たすソリューションを見つけることができませんでした。それからZiexを見て、私は驚きました。JSXアプローチがJavaScript以外でもできるとは思いもよりませんでした。Nurul HudaがZigで構築した開発者体験は、私には想像もつかないものでした。それは私のニーズに直接合致しませんでした。WASMフレームワークは欲しくなかったからです。しかし、その構文とZigコードをテンプレートに統合する彼の方法は、私にいくつかのアイデアを与えてくれました。その後、さらに調査した結果、GoのTemplを発見し、私が何を構築したいのかは明白になりました。そこで、zt(名前付けは得意ではありません、すみません)を紹介します。これはTemplのZigへの適応です。テンプレートはzig build中に解析され、Zigコードにトランスパイルされます。つまり、アプリコードから直接インポートできますが、おそらくさらに重要なことに、テンプレートはアプリコードをインポートできます。その結果、すべてのパラメータは完全に型付けできます。レンダリングできないものがあれば、Zigコンパイラが教えてくれます。生成されたコードは非常にシンプルで、すべてstd.Io.Writerに書き込むだけです。ソケットに直接書き込むことも、バッファに出力を蓄積することもできます。お好きな方を選んでください。テンプレートは次のようになります。 const User = @import("../models.zig").User; pub templ Layout(title: []const u8) { <!DOCTYPE html> <html> <head> <title>{title}</title> </head> <body> @children </body> </html> } pub templ HomePage(user: ?User) { @Layout("Home") { if (user) |u| { <h1>Welcome back, {u.name}!</h1> <a href="/users/{u.id}">Your profile</a> } else { <h1>Welcome, guest!</h1> <a href="/login">Log in</a> } } } そして、Zigコードから次のように使用します。 const std = @import("std"); const User = @import("models.zig").User; const pages = @import("templates/pages.zig"); pub fn main(init: std.process.Init) !void { var buf: [4096]u8 = undefined; var stdout = std.Io.File.stdout().writer(init.io, &buf); const w = &stdout.interface; const user: User = .{ .id = 1, .name = "Lukas" }; try pages.HomePage.render(.{user}, w); try w.flush(); } Dusty内でHTMLページをレンダリングするラッパーを使用したい場合は、次のようにします。 try res.render(.html, pages.HomePage, .{user}); ちなみに、上記のメインスニペットは、std.Ioのキャンセルが関係するパターンをどこかで使用した場合、エラーハンドリングに非常に明白でないバグがあります。おそらくそれについてのブログ記事を書くでしょうが、興味がある場合は、このZiggitスレッドを確認してください。std.Io.WriterのハンドリングをラップするDustyのrender関数がそれを解決します。すべての動的値は、テキストと属性の両方でデフォルトでHTMLエスケープされ、hrefやsrcのようなURL属性の値も危険なスキームがないかチェックされます。本当に生のHTMLを出力する必要がある場合は、{!value}で明示的に要求する必要があり、テンプレートで簡単に見つけることができます。パーサーはすべての要素が正しく閉じられていることを要求するため、誤って壊れたマークアップを生成することはありません。すべてから保護してくれるわけではありませんが、XSSの最も一般的なソースを削除します。私が最も気に入っているのは、テンプレートエラーが単なるZigコンパイラエラーであることです。上記のテンプレートでタイプミスをして{u.name}の代わりに{u.nmae}と書いた場合、zig buildが教えてくれます。 src/templates/pages.zig:45:43: error: no field named 'nmae' in struct 'models.User' try zt.writeEscaped(writer, u.nmae); // pages.zt:18:31 ^~~~ src/models.zig:1:18: note: struct declared here pub const User = struct { ^~~~~~ エラーは生成されたコードを指しますが、各行には元のテンプレートの場所を示すコメントが付いています。呼び出し側でも同様で、HomePageが?Userを期待している場所に文字列を渡そうとすると、コンパイルできません。ここで示した基本的な機能を超えて、再利用可能なコンポーネントを構築して値として渡すことができ、@childrenを使用すると他のテンプレートをラップするレイアウトを構築でき、これは他の言語でテンプレート継承に使用されるものをカバーします。制御フローは単なるZigなので、if、for、switchはペイロードキャプチャを含め、期待どおりに機能します。すべてがアロケーションなしで、一連の書き込みにコンパイルされます。DustyのTODO例を参照できます。これは、このライブラリと、動的なSPAを構築するために必要なDustyの他の部分を紹介する、htmxベースのシンプルなWebアプリです。構文の1つの部分はまだ未定です。ブロックレベルの制御フローは、Templの例に従ってプレーンなZigとして記述されているため、if、for、switchで始まる行はコードとして解析されます。つまり、「パスワードを忘れました…」のようなテキストが単独の行にあると解析エラーになり、要素でラップするか、文字列式として記述する必要があります。RazorやTwirlに似た@if、@for、@switchに切り替えることを検討しており、テンプレートが少しノイズが多くなるという代償を払って曖昧さを解消します。これについて意見があれば、このGitHub issueにコメントを残してください。すでに複数の人が使用していますが、プロジェクトはまだ成熟していないため、あらゆるフィードバックを歓迎します。問題を見つけた場合はGitHub issueを開くか、Zig Discordで私にpingしてください。