我有一个使用 App Router 设置的 Nextjs 13 前端和一个使用 Doorkeeper 进行身份验证的仅 Rails 7 API 后端。
我有一个新的产品表单,它利用 Nextjs“服务器操作”将 POST 请求发送到 Rails 后端的
products#create
路由。我想使用服务器操作,以便在创建产品后我可以调用 nextjs revalidatePath()
来清除缓存并使用更新的产品列表刷新 UI。
但是,由于我使用配置为通过 HttpOnly cookie 进行身份验证的 Doorkeeper,并且
products#create
方法是受保护的路由,因此我似乎不知道如何传递所需的 access_token
cookie。
似乎是:
或,
我试图找到一个 nextjs 中间件解决方案,但即使我可以在控制台记录
request.headers
时看到包含 access_token 并且它似乎位于 devTools 中的网络请求中,但 cookie 没有到达后端的 Doorkeeper。当我从服务器操作中删除 revalidatePath() 并删除“使用服务器”标志时,cookie 确实会通过(因此它不再是服务器操作)。
这是第 22 条军规场景吗?还是我在 Nextjs 文档中遗漏了某些内容?我猜测 Nextjs 服务器无法访问浏览器中的 HttpOnly cookie,因此在使用服务器操作时可能无法将它们包含在服务器端。但这让我很困惑为什么 cookie 会出现在 devTools 的网络选项卡中。
我希望有人可能使用过类似的设置,并找到了我可以在这种情况下实施的最佳实践或解决方法。
非常感谢!
这是向
products#create
发送获取请求的服务器操作:
// actions.ts
'use server'
export const createProduct = async (data: FormData) => {
const url = `${ baseApiUrl() }/v1/products`;
const response = await fetch(url, {
credentials: 'include',
method: 'POST',
headers: {
'Authorization': `Basic ${ doorkeeperCredentials() }`,
},
body: configureData(data)
});
revalidatePath('/products');
return response.json();
};
这是
ProductForm
组件:
// product-form.component.tsx
'use client'
// library
import { useState, useEffect } from "react";
// api
import { createProduct } from "../../api/products-api";
import { Category } from "@/app/categories/page";
import { getAllCategories } from "@/app/api/categories-api";
const ProductForm = () => {
// state
const [ loading, setLoading ] = useState(true);
const [ categories, setCategories ] = useState<Category[] | null>(null)
useEffect(() => {
const getCategories = async () => {
const response = await getAllCategories();
const categories: Category[] = await response.json();
setCategories(categories);
};
categories ? setLoading(false) : getCategories();
}, [ categories ])
if (loading) return <p>Loading...</p>;
return (
<form
id="product"
className="product-form"
action={ formData => createProduct(formData) }
>
{/* product name */}
<label
className="product-form__label"
htmlFor="name"
>
Name
</label>
<input
id="name"
className="product-form__input"
type="text"
autoComplete="false"
name="product[name]"
/>
{/* product description */}
<label
className="product-form__label"
htmlFor="short-description"
>
Description
</label>
<textarea
id="short-description"
className="product-form__textarea"
name="product[short_description]"
/>
{/* product images */}
<label
className="product-form__label"
htmlFor="product-images"
>
Images
</label>
<input
id="product-images"
className="product-form__attach-button"
type="file"
name="product[product_images][]"
multiple
/>
<div className="category-select">
{ categories && categories.map((category) =>
<div key={ category.id }>
<input
id={ `category-${ category.name }` }
type='checkbox'
value={ category.id }
name={ `product[category_ids][]`}
/>
<label htmlFor={ `category-${ category.name }` }>
{ category.name }
</label>
</div>
)}
</div>
{/* submit button */}
<button
className="product-form__button"
type='submit'
>
Submit
</button>
</form>
)
};
export default ProductForm;
这是我的下一个配置:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true
},
images: {
domains: [
'localhost'
]
}
}
module.exports = nextConfig
我找到了解决此问题的解决方法。感觉有点笨重,所以如果有更干净的解决方案或更好的实践,我很想知道。
在单独的
server-actions.ts
文件中,我导出了一个仅调用 revalidatePath()
的异步函数。这显然感觉多余,但它遵循 Nextjs 协议在客户端组件内使用服务器操作:要么从顶部带有“use server”的文件导出操作,要么直接传递带有“use server”的异步函数它从服务器组件到客户端组件。
由于我想在一些地方重用此功能,所以我选择从单独的文件导出。
我在 ProductForm 组件中以相同的方式调用我的
createProduct(formData)
函数:
<form
id="product"
className="product-form"
action={ formData => createProduct(formData) }
>
{/* product name */}
<label
className="product-form__label"
htmlFor="name"
>
Name
</label>
<input
id="name"
className="product-form__input"
type="text"
autoComplete="false"
name="product[name]"
/>
...
{/* submit button */}
<button
className="product-form__button"
type='submit'
>
Submit
</button>
</form>
然后在我的
products-api.ts
文件中导入我在 revalidate
中定义的 server-actions.ts
函数,并在 createProduct() 函数中调用它。这样,createProduct 仍然作为客户端函数运行,将 HttpOnly cookie 传递给 Rails 后端的 Doorkeeper,然后在成功创建产品后强制进行服务器端重新验证。
export const createProduct = async (data: FormData) => {
const url = `${ baseApiUrl() }/v1/products`;
const response = await fetch(url, {
credentials: 'include',
method: 'POST',
headers: {
'Authorization': `Basic ${ doorkeeperCredentials() }`,
},
body: configureData(data)
});
revalidate('/');
return response.json();
};
我不确定像这样组合客户端和服务器操作是否遵循 Nextjs 最佳实践,但根据我的需求,我现在一切都按我希望的那样工作。