コンテンツにスキップ

サーバーアクション

更新するフォームにはアドレスが要ります。手で書いたアドレスは、狙っている関数と 突き合わされることのない文字列です。ハンドラの名前を変えても、誰かがクリックする まで何も落ちません。

server-action は、代わりに関数を名指しします。

package users
export component Page(user: User): html {
<form server-action="Retire">
<label>理由 <input type="text" name="reason" /></label>
<button type="submit">退会させる</button>
</form>
}
package users
// Retire は、名指ししているテンプレートの隣、ルートパッケージにある export された
// ハンドラです。まわりには何も生成されません。登録だけがすべてです。
func Retire(w http.ResponseWriter, r *http.Request) {
request, err := pw.Parse[retireRequest](r)
if err != nil {
pw.WriteProblem(w, r, err)
return
}
if err := retire(r.Context(), pw.PathValue(r, "id"), request.Reason); err != nil {
pw.WriteProblem(w, r, err)
return
}
}
type retireRequest struct {
Reason string `json:"reason" check:"required"`
}

Go 側で Retire の名前を変えると、それを参照している属性の位置で生成が落ちます。 パスではなく関数を名指しする理由はこれがすべてです。

server-action を持つ <form> は、ブラウザがフレームワークのランタイムを 走らせていても走らせていなくても動きます。これを推す理由がそれです。

生成が書くのは method="post"、ハンドラを名指しする hidden フィールド、そして CSRF トークン。action意図的に書きませんaction の無いフォームは ドキュメント自身の URL に送信するからで、その URL にはこのページのパス パラメータが既に埋まっています。だから同じマークアップが2つのことをします。

  • JavaScript が無ければ、ブラウザがネイティブに POST してハンドラが応えます。
  • ランタイムが読み込まれていれば submit が横取りされ、fetch で POST され、 レスポンスはページ全体ではなくリージョン として適用されます。

設定するものはありません。どちらの経路になるかはランタイムの有無が決めます。 リンクがリンクだから動き、ランタイムはそれを速くするだけ、というのと同じです。

裸のボタンはスクリプト無しの経路を失う

Section titled “裸のボタンはスクリプト無しの経路を失う”

server-action はどの要素にも書けます。フォーム以外では、ランタイムが読む属性 1つに lowering されます。

<button server-action="Rename">名前を変更</button>

スクリプトを切ると、このボタンは何もしません。lowering で埋められる話ではなく、 フォームの外のボタンを起動するものが HTML に存在しないからです。

送信先も違い、そちらのほうが運ぶものが少ない。フォームはページ URL へ行くので pw.PathValue(r, "id") で id が読めます。裸のボタンは /_action/<hash>/Rename へ行き、これはコンパイル時定数でパスパラメータを1つも含みません。この経路で 届いたハンドラは、どのユーザーの話なのかを知る手立てがありません。

つまり サーバーアクションはフォームに置いてください。フィールドもインスタンス も本当に無く、スクリプト無しでは何も起きないことを受け入れた場合だけが例外です。

レスポンス全体を自分で組み立てる、通常の http.HandlerFunc です。登録なしで httptest から直接呼べます。

何も書かないことには意味があります。フォーム側の入口は 303 でページに戻す ので、リロードで再送信されず、アドレスバーはページを指したままです。ステータス・ ヘッダ・ボディのどれかを書けば、代わりにそのレスポンスがそのまま立ちます。他所へ リダイレクトする、バリデーションエラー付きでページをその場で描画する、ストリーム する、はこれで表現します。

アドレスは何の権限も与えません。どちらの入口も公開されているので、ハンドラは他の ルートと同じく自分で認証と認可を行います。ルートパッケージの export された ハンドラ形の関数は、テンプレートが名指ししているかどうかに関わらずエンドポイントを 持ちます。持たせたくないものは小文字にしてください。

レスポンス全体を組み立てられるため、呼び出し元ごとに適切な形式で応答できる、 ということです。

func Rename(w http.ResponseWriter, r *http.Request) {
// …更新…
switch {
case pw.WantsValue(r):
// スクリプトが名前で呼び、答えを受け取る先を持っている。
pw.WriteAPI(w, r, renamed{Name: request.Name})
case pw.WantsUpdate(r):
// ランタイムがジェスチャを横取りした。変わったリージョンを返せば
// その場で適用される。
pw.WriteUpdate(w, r, pw.Replace("name", BindName(request.Name)))
default:
// ネイティブ送信。ページを待っているドキュメントがある。
pw.RedirectSeeOther(w, r, "/users/"+id)
}
}

どちらも訊かなければ、1つのレスポンスが全員に返ります。返すものが無いハンドラには それが正解です。最初の1つを訊けば、フォーム送信に JSON ドキュメントを見せずに 済みます。

レスポンスを自由に組み立てられることは、型付きの関数シグネチャだけでは表現できません。 リダイレクト・条件付きステータス・ダウンロード・ストリームはどれも正当な答えであり、 固定の戻り値では覆えません。

入力には型が付きますpw.Parse が構造体を回収し、生成がフォームのフィールド名を そのフィールドと突き合わせます。型が付かないのは署名と戻り値で、それがフォームに 応えるための代金です。

JavaScript からの呼び出しだけに応答する関数なら、この制約を避けられます。次の形式を使います。

フォームにはレスポンスで答えるしかありません。スクリプトが問い合わせるときは違って、 欲しいのは値です。普通の Go の関数を宣言すれば、それになります。

package users
// 公開しているのは宣言です。関数の上に置けば、その関数に出会った読み手が
// 到達可能だと分かります。
var _ = pw.ServerAction(profile)
func profile(ctx context.Context, id string) (Profile, error) {
return load(ctx, id)
}
const p = await actions.profile({ id: "42" }); // デコードされた Profile

両端に型が付きます。引数は呼び出しのペイロードから名前で届き、結果はエンコードされて 返り、エラーはフレームワークが既に対応付けているステータスの problem レスポンスになります。

宣言が admit の条件であることから、4つが従います。

  • 関数は非公開でよい。 ここでは存在するだけでは何も公開されません。上のハンドラ形状の ちょうど逆です。
  • どんな署名でもよい。 先頭の context.Context は任意で、リクエストのものを 受け取ります。データベースハンドルもログイン中の読み手も、どちらも手が届きます。
  • 公開名で呼ばれます。 GetUseractions.getUser です。pw.ServerAction に 文字列を渡せば別の名前で公開できます。リネームで動かしてはいけない名前がそれです。
  • テンプレートからは名指せません。 server-action="profile" は生成エラーです。 フォームが到達しても、描画できない値を見せられるだけだからです。

スクリプトが答えを欲しいときはこちら、フォームがレスポンスを欲しいときはハンドラ形状。 1つのページが両方を持って構いません。

拒否された送信は 4xx を返し、そこに載っているリージョンがバリデーション エラーです。ステータスが何であれランタイムはそれを適用します。返している理由が それだからです。再描画されたフォームが何を見せるかは フォームにあります。

生成されたフォームは CSRF トークンを運び、その出どころが security.csrf です。

チェックが on なら、トークンはログインの有無に関わらず全訪問者に発行されます。 セッションが匿名のあいだシークレットは sealed cookie に乗るので、フォームのある ページを読んだだけのクローラにサーバレコードは書かれません。必要なのは session.enabled のほうです。シークレットはセッションスロットなので。

security.csrf.enabled = false なら、フォームは描画されます。ただしトークン フィールドは空で、送信を検証するものは何もありません。チェックを切るとはそういう ことです。そして、サーバーアクションの採用は、プロジェクトがそれまで持っていな かった unsafe なフォームを手に入れる瞬間でもある、と知っておく価値があります。

フォームアクションを宣言したテンプレートがあると、生成はそのページ自身のパスに GET と並べて POST を登録します。

アプリケーションが既に同じパスへ POST を手登録していると、起動時に重複で panic します。Go のルータが両方の登録位置を名指しし、片方は生成レジストリです。自分の 登録を外すか、その更新をアクションの側に寄せてください。

サーバ側で何も変わらないなら、それはアクションではありません。開閉は <details>、ダイアログは <dialog>、そして今いるページを絞り込む検索フォームは ランタイムが既に横取りしている普通の GET フォームです。

ブラウザで走るなら、それはサーバー側ではなくクライアント側のハンドラです。書き方も 解決のされ方も同じで、出どころがコンポーネント自身のスクリプトブロックになります。 コンポーネントスクリプトを 参照してください。

更新にゲートをかけたいなら — 確認や、ブラウザにしかできない判定 — クライアント ハンドラを要素に置き、server-action は置かずに、JavaScript からアクションを 呼びます。両方を書いたテンプレートはハンドラを走らせてからアクションを必ず発行 します。サーバーアクションを呼ぶを 参照してください。

変わるけれどページツリーの中にいないなら、普通のルートを書いてください。サーバー アクションはページの実装詳細です。OpenAPI ドキュメントには載らず、バージョンを 付けるものも無いので、ページの外の呼び出し側が掴める契約がありません。

属性の正確な lowering は ページツリーの server-actionに、 ハンドラが置かれるルートパッケージのモデルは 発見型ルーティングにあります。