如何让 Express server 正确配合 react-admin getOne()?
结论
问题不在于 Express 响应太快,也不需要人为添加延迟,而是 create 接口返回的数据结构不符合 react-admin 的 Data Provider 契约。
创建成功后,react-admin 需要收到:
{
data: {
id: 123,
// 其他字段
}
}
当前服务端很可能直接返回了 res.insertId,也就是一个数字。因此,Data Provider 读取 json.id 时得到的是 undefined:
id: json.id
随后执行 redirect="show" 时,由于没有可用的记录 ID,客户端最终会发出类似这样的请求:
GET /vets/undefined
解决办法是让 Express 的创建接口返回一个包含 id 的 JSON 对象,同时确保 getOne 返回单条对象,而不是数组。
为什么 async/await 没有解决问题
下面的代码并没有真正等待查询完成:
await sql.query("INSERT INTO vets set ?", newVet, function(err, res) {
// ...
});
await 只能等待 Promise。以回调方式调用 sql.query() 时,如果数据库驱动没有返回代表查询结果的 Promise,这里的 await 基本不起作用。
但这不是 ID 丢失的直接原因。只要 Express 在数据库回调中调用 res.json(),HTTP 响应仍会等到插入完成后再发送。这里的问题在于响应内容的结构,而不是发送响应的时机。
修正创建接口的响应
Model 层可以继续通过回调返回 insertId,但不需要声明为 async:
Vet.createVet = function (newVet, result) {
sql.query("INSERT INTO vets SET ?", newVet, function (err, res) {
if (err) {
console.error("createVet error:", err);
return result(err, null);
}
result(null, res.insertId);
});
};
然后在 Express controller 中将 ID 包装成对象:
exports.create = function (req, res) {
Vet.createVet(req.body, function (err, insertId) {
if (err) {
return res.status(500).json({
error: err.message
});
}
return res.status(201).json({
id: insertId
});
});
};
客户端收到的 JSON 将是:
{
"id": 123
}
这样,现有 Data Provider 的 create 实现就能正确读取 json.id:
create: (resource, params) =>
httpClient(`${apiUrl}/${resource}`, {
method: 'POST',
body: JSON.stringify(params.data),
}).then(({ json }) => ({
data: {
...params.data,
id: json.id,
},
})),
还可以检查响应中是否包含 ID,避免接口格式错误时继续跳转:
create: async (resource, params) => {
const { json } = await httpClient(`${apiUrl}/${resource}`, {
method: 'POST',
body: JSON.stringify(params.data),
});
if (json.id === undefined || json.id === null) {
throw new Error('Create response does not contain an id');
}
return {
data: {
...params.data,
id: json.id,
},
};
},
使用 .then() 或 async/await 都可以,功能上没有区别。最终返回的 Promise 必须解析为一个包含 data.id 的对象。
如果服务端当前直接返回数字
假设 Express 当前执行的是:
res.json(insertId);
客户端收到的 json 本身就是 ID,例如:
123
这种情况下,可以暂时修改 Data Provider:
create: (resource, params) =>
httpClient(`${apiUrl}/${resource}`, {
method: 'POST',
body: JSON.stringify(params.data),
}).then(({ json }) => ({
data: {
...params.data,
id: json,
},
})),
更稳妥的做法仍然是修改服务端,让它返回:
{
"id": 123
}
对象形式更便于以后扩展,也不容易与其他响应格式混淆。
修正 getOne 的返回值
MySQL 查询即使只匹配到一条记录,通常也会返回数组:
[
{
"id": 123,
"FirstName": "Adam"
}
]
而 react-admin 要求 getOne 中的 data 是单个对象:
{
data: {
id: 123,
FirstName: "Adam"
}
}
因此,服务端最好直接提取第一条记录:
Vet.getVetById = function (vetId, result) {
sql.query(
"SELECT * FROM vets WHERE id = ?",
[vetId],
function (err, rows) {
if (err) {
console.error("getVetById error:", err);
return result(err, null);
}
result(null, rows[0] || null);
}
);
};
Controller 还需要处理记录不存在的情况:
exports.findOne = function (req, res) {
Vet.getVetById(req.params.id, function (err, vet) {
if (err) {
return res.status(500).json({
error: err.message
});
}
if (!vet) {
return res.status(404).json({
error: 'Vet not found'
});
}
return res.json(vet);
});
};
这样,原来的 getOne 可以保持不变:
getOne: (resource, params) =>
httpClient(`${apiUrl}/${resource}/${params.id}`)
.then(({ json }) => ({
data: json,
})),
如果暂时不能修改服务端,也可以在 Data Provider 中提取数组的第一项:
getOne: (resource, params) =>
httpClient(`${apiUrl}/${resource}/${params.id}`)
.then(({ json }) => ({
data: Array.isArray(json) ? json[0] : json,
})),
不过,后端接口最好采用统一规则:单条查询返回对象,列表查询返回数组。
推荐的完整响应流程
客户端发出创建请求:
POST /vets
服务端响应:
{
"id": 123
}
Data Provider 返回给 react-admin:
{
data: {
...params.data,
id: 123
}
}
随后,react-admin 使用该 ID 跳转并发出请求:
GET /vets/123
服务端返回单条记录:
{
"id": 123,
"FirstName": "Adam",
"Surname": "Smith"
}
还需要注意的细节
字段名大小写必须一致
创建表单使用的是:
<SelectInput source="sex" />
编辑表单使用的是:
<TextInput source="Sex" />
JavaScript 属性名区分大小写。应根据数据库和 API 的实际字段名,将两处统一为 sex 或 Sex。否则,即使记录成功加载,该字段也可能显示为空。
CORS 响应头不能通过请求头设置
下面的代码只是添加了一个名为 X-Custom-Header 的请求头,并没有配置 CORS:
options.headers.set(
'X-Custom-Header',
'Access-Control-Allow-Headers',
'Access-Control-Allow-Origin',
'*',
);
Headers.set() 只接收两个参数,后面的参数不会产生预期效果。Access-Control-Allow-Origin 等 CORS 响应头应由 Express 设置,例如使用 cors middleware。这个问题与 ID 丢失没有直接关系,但当前写法容易让人误以为 CORS 已经配置完成。
redirect="show" 需要对应的 Show 页面
redirect="show" 会在创建成功后跳转到该记录的 Show 路由。如果实际需要跳转到编辑页面,应使用:
<TabbedForm redirect="edit">
无论跳转到 show 还是 edit,创建结果中都必须有有效的 data.id。
因此,不需要等待几秒,也不应使用 setTimeout。只需统一接口契约:创建接口返回 { id: insertId },Data Provider 返回 { data: { ..., id } },getOne 则返回 { data: 单条对象 }。
备注:内容仅供参考。
