如果 `accept-language` header 中没有国家代码,何时询问客户他们的首选货币?
When to ask the client their preferred currency if no country code in `accept-language` header?
我的代码在做什么:确定客户货币
我正在从 accept-language
header 获取客户的首选语言环境。从 accept-language
header,我得到语言和国家代码来帮助我找出他们的首选货币。 en-US
为美元,en-CA
加元等
这是获取首选语言环境的中间件代码:
const getPreferredLocale = (acceptLanguageHeader) => {
const locales = acceptLanguageHeader
.split(/(\b, \b|\b,\b|\b;q=\b)/g)
.filter((el) => el !== ',' && el !== ', ' && el !== ';q=')
.reduce(
(a, c, i, arr) =>
Number.isNaN(Number(c))
? [...a, { locale: c, q: Number.isNaN(Number(arr[i + 1])) ? '1' : arr[i + 1] }]
: a,
[]
)
.sort((a, b) => (a.q > b.q ? -1 : 1));
return (
locales.find((el) => el.locale.match(/-[A-Z]{2}/g) && el.locale.match(/-[A-Z]{2}/g)).locale ||
locales[0].locale
);
};
const makeLocaleObj = (locale) => ({
locale,
countryCode: locale.match(/(?<=\-)[A-Z]*/g)[0],
languageCode: locale.match(/[^-]*/)[0],
});
const setLocaleCookie = (req, res, next) => {
const cookieLocale = req.cookies.locale;
if (!cookieLocale) {
const locale = getPreferredLocale(req.headers['accept-language']);
const localeObj = makeLocaleObj(locale);
res.cookie('locale', JSON.stringify(localeObj), { maxAge: new Date() * 0.001 + 300 });
req.countryCode = localeObj.countryCode; // set for currency middleware
}
next();
};
app.use(setLocaleCookie);
在另一个中间件中,我使用国家代码来确定货币。
问题
但有时用户可能只有 header 中的语言代码,而没有国家代码,例如 en
中的英语。您需要国家/地区来确定货币。那你是做什么的?
在这种情况下,您要么必须
- 根据语言猜测国家/地区 - 对某些语言来说容易,对其他语言来说更难
- 让客户端请求
https://extreme-ip-lookup.com/json/
并从响应中获取国家代码
- 要求客户指定他们的货币
我要选择最后两个中的任何一个。但是我不知道什么时候做这些。
如果这是一条路线我能做什么
如果 cookie 是由像 /setCookie
这样的路由设置的,那么这将很容易:响应可以向客户端指定下一步是什么。例如,服务器可以发送带有 JSON object 的 200 状态,例如 {stillNeedCountry: true}
。然后客户可以知道需要采取更多步骤。
但这是一个通用的中间件
但 cookie 通常不会在特定的路由请求中设置。它们是在从该客户端向每个请求调用的中间件中的服务器发出的任何第一个请求上设置的。这让我很困惑。我们可以在中间件中检测到没有国家代码,但是然后呢?
解决方案?
我是否劫持请求并直接从中间件发送响应告诉前端该做什么?这看起来很复杂,因为我们必须在前端设置每个获取请求来处理此响应。
有什么可能的解决方案?
我在此 post 中的假设是不正确的。
This conversation with Andy Palmer 为我澄清了很多。
- 我认为中间件是传递请求的任何函数,无论它是否仅在某些端点上。但事实证明,中间件是所有请求都经过的功能。我称其为“通用中间件”。
- 我认为在某些“中间件魔术”中始终需要在后端设置 cookie。他们不。它们可以使用特定的路由请求和在前端设置。
- 我误解了前端、后端和中间件的工作。
我认为 cookie 需要始终在后端设置,因为我对 cookie 的唯一其他体验是使用 express-session
,其中 cookie 设置在后端中间件中。这只是我根据之前看到的 cookie 的使用方式做出的假设。
You don't set the cookie in middleware because it's an application/business logic concern, not an infrastructure concern
...
...currency choice is a user choice, so is set by the user.
...
Use middleware to annotate a request with infrastructure things that the application doesn't need to do.
...
You could use middleware to annotate the request with the selected currency, but it feels a bit application-specific.
E.g. a middleware to extract country and language from the accept-language headers
-安迪
我可以使用特定的路由查询是否设置了cookie并决定如何进行。然后我可以在前端做一些事情,比如要求客户根据我们从语言环境获得的提示从货币列表中指定他们的首选货币。
app.get('/hint_currency', (req, res) => {
res.send(req.cookies.locale || req.locale);
});
function App() {
const [user, setUser] = useState(null);
const parsedCookies = () => {
const str = decodeURIComponent(document.cookie).split('; ');
const result = {};
for (let i = 0; i < str.length; i++) {
const cur = str[i].split('=');
result[cur[0]] = cur[1];
}
return result;
};
const chooseCurrency = (locale) => {
if (locale.countryCode) {
const currencies = getCurrencies(locale.countryCode);
//replace with form to select currency and set document.cookie
if (currencies.length > 1)
return alert('Here we would ask the user to pick currency: ' + currencies.join(', '));
document.cookie = `currency= ${currencies[0]}`; // give the user a way to change the currency
} else {
//replace with form to select currency based on language and set document.cookie
alert(
`Here the user would pick currency from list of currencies. Currencies used in countries where people speak languageCode: "${locale.languageCode}" could be at top of list`
);
}
};
const fetchCurrency = () => {
if (!user?.currency && !parsedCookies().currency) {
fetch('/hint_currency')
.then((res) => {
if (res.status === 204) return null;
return res.text();
})
.then((text) => {
const locale = JSON.parse(text);
chooseCurrency(locale);
});
}
};
useEffect(() => {
fetchCurrency();
}, []);
//...
或者,我意识到我可以在第一次获取请求后处理设置货币,使用 document.cookie.locale
而不是在 '/hint_currency' 的响应之后。
function App() {
const [user, setUser] = useState(null);
const parsedCookies = () => {
const str = decodeURIComponent(document.cookie).split('; ');
const result = {};
for (let i = 0; i < str.length; i++) {
const cur = str[i].split('=');
result[cur[0]] = cur[1];
}
return result;
};
const chooseCurrency = (locale) => {
if (locale.countryCode) {
const currencies = getCurrencies(locale.countryCode);
//replace with form to select currency and set document.cookie
if (currencies.length > 1)
return alert('Here we would ask the user to pick currency: ' + currencies.join(', '));
document.cookie = `currency= ${currencies[0]}`; // give the user a way to change the currency
} else {
//replace with form to select currency based on language and set document.cookie
alert(
`Here the user would pick currency from list of currencies. Currencies used in countries where people speak languageCode: "${locale.languageCode}" could be at top of list`
);
}
};
const fetchUser = () => {
return fetch('/users/current')
.then((res) => {
if (res.status === 204) return null;
return res.json();
})
.then((user) => {
setUser(user);
return user;
});
};
useEffect(() => {
fetchUser().then((usr) => {
const cookies = parsedCookies();
if (!usr?.currency || !cookies.currency) chooseCurrency(JSON.parse(cookies.locale));
else if (usr?.currency) document.cookie.currency = usr.currency;
});
}, []);
//...
您还可以将货币存储在会话中。
Generally, you'd probably store the currency (and other user data) in a session store, and the cookie would identify the session.
Then the session store middleware would retrieve and annotate the request with the user data. That's the usual compromise; the middleware only knows about session state, it's not making business decisions.
Your application asks for request.session.currency
我的代码在做什么:确定客户货币
我正在从 accept-language
header 获取客户的首选语言环境。从 accept-language
header,我得到语言和国家代码来帮助我找出他们的首选货币。 en-US
为美元,en-CA
加元等
这是获取首选语言环境的中间件代码:
const getPreferredLocale = (acceptLanguageHeader) => {
const locales = acceptLanguageHeader
.split(/(\b, \b|\b,\b|\b;q=\b)/g)
.filter((el) => el !== ',' && el !== ', ' && el !== ';q=')
.reduce(
(a, c, i, arr) =>
Number.isNaN(Number(c))
? [...a, { locale: c, q: Number.isNaN(Number(arr[i + 1])) ? '1' : arr[i + 1] }]
: a,
[]
)
.sort((a, b) => (a.q > b.q ? -1 : 1));
return (
locales.find((el) => el.locale.match(/-[A-Z]{2}/g) && el.locale.match(/-[A-Z]{2}/g)).locale ||
locales[0].locale
);
};
const makeLocaleObj = (locale) => ({
locale,
countryCode: locale.match(/(?<=\-)[A-Z]*/g)[0],
languageCode: locale.match(/[^-]*/)[0],
});
const setLocaleCookie = (req, res, next) => {
const cookieLocale = req.cookies.locale;
if (!cookieLocale) {
const locale = getPreferredLocale(req.headers['accept-language']);
const localeObj = makeLocaleObj(locale);
res.cookie('locale', JSON.stringify(localeObj), { maxAge: new Date() * 0.001 + 300 });
req.countryCode = localeObj.countryCode; // set for currency middleware
}
next();
};
app.use(setLocaleCookie);
在另一个中间件中,我使用国家代码来确定货币。
问题
但有时用户可能只有 header 中的语言代码,而没有国家代码,例如 en
中的英语。您需要国家/地区来确定货币。那你是做什么的?
在这种情况下,您要么必须
- 根据语言猜测国家/地区 - 对某些语言来说容易,对其他语言来说更难
- 让客户端请求
https://extreme-ip-lookup.com/json/
并从响应中获取国家代码 - 要求客户指定他们的货币
我要选择最后两个中的任何一个。但是我不知道什么时候做这些。
如果这是一条路线我能做什么
如果 cookie 是由像 /setCookie
这样的路由设置的,那么这将很容易:响应可以向客户端指定下一步是什么。例如,服务器可以发送带有 JSON object 的 200 状态,例如 {stillNeedCountry: true}
。然后客户可以知道需要采取更多步骤。
但这是一个通用的中间件
但 cookie 通常不会在特定的路由请求中设置。它们是在从该客户端向每个请求调用的中间件中的服务器发出的任何第一个请求上设置的。这让我很困惑。我们可以在中间件中检测到没有国家代码,但是然后呢?
解决方案?
我是否劫持请求并直接从中间件发送响应告诉前端该做什么?这看起来很复杂,因为我们必须在前端设置每个获取请求来处理此响应。
有什么可能的解决方案?
我在此 post 中的假设是不正确的。
This conversation with Andy Palmer 为我澄清了很多。
- 我认为中间件是传递请求的任何函数,无论它是否仅在某些端点上。但事实证明,中间件是所有请求都经过的功能。我称其为“通用中间件”。
- 我认为在某些“中间件魔术”中始终需要在后端设置 cookie。他们不。它们可以使用特定的路由请求和在前端设置。
- 我误解了前端、后端和中间件的工作。
我认为 cookie 需要始终在后端设置,因为我对 cookie 的唯一其他体验是使用 express-session
,其中 cookie 设置在后端中间件中。这只是我根据之前看到的 cookie 的使用方式做出的假设。
You don't set the cookie in middleware because it's an application/business logic concern, not an infrastructure concern
...
...currency choice is a user choice, so is set by the user.
...
Use middleware to annotate a request with infrastructure things that the application doesn't need to do.
...
You could use middleware to annotate the request with the selected currency, but it feels a bit application-specific.E.g. a middleware to extract country and language from the accept-language headers
-安迪
我可以使用特定的路由查询是否设置了cookie并决定如何进行。然后我可以在前端做一些事情,比如要求客户根据我们从语言环境获得的提示从货币列表中指定他们的首选货币。
app.get('/hint_currency', (req, res) => {
res.send(req.cookies.locale || req.locale);
});
function App() {
const [user, setUser] = useState(null);
const parsedCookies = () => {
const str = decodeURIComponent(document.cookie).split('; ');
const result = {};
for (let i = 0; i < str.length; i++) {
const cur = str[i].split('=');
result[cur[0]] = cur[1];
}
return result;
};
const chooseCurrency = (locale) => {
if (locale.countryCode) {
const currencies = getCurrencies(locale.countryCode);
//replace with form to select currency and set document.cookie
if (currencies.length > 1)
return alert('Here we would ask the user to pick currency: ' + currencies.join(', '));
document.cookie = `currency= ${currencies[0]}`; // give the user a way to change the currency
} else {
//replace with form to select currency based on language and set document.cookie
alert(
`Here the user would pick currency from list of currencies. Currencies used in countries where people speak languageCode: "${locale.languageCode}" could be at top of list`
);
}
};
const fetchCurrency = () => {
if (!user?.currency && !parsedCookies().currency) {
fetch('/hint_currency')
.then((res) => {
if (res.status === 204) return null;
return res.text();
})
.then((text) => {
const locale = JSON.parse(text);
chooseCurrency(locale);
});
}
};
useEffect(() => {
fetchCurrency();
}, []);
//...
或者,我意识到我可以在第一次获取请求后处理设置货币,使用 document.cookie.locale
而不是在 '/hint_currency' 的响应之后。
function App() {
const [user, setUser] = useState(null);
const parsedCookies = () => {
const str = decodeURIComponent(document.cookie).split('; ');
const result = {};
for (let i = 0; i < str.length; i++) {
const cur = str[i].split('=');
result[cur[0]] = cur[1];
}
return result;
};
const chooseCurrency = (locale) => {
if (locale.countryCode) {
const currencies = getCurrencies(locale.countryCode);
//replace with form to select currency and set document.cookie
if (currencies.length > 1)
return alert('Here we would ask the user to pick currency: ' + currencies.join(', '));
document.cookie = `currency= ${currencies[0]}`; // give the user a way to change the currency
} else {
//replace with form to select currency based on language and set document.cookie
alert(
`Here the user would pick currency from list of currencies. Currencies used in countries where people speak languageCode: "${locale.languageCode}" could be at top of list`
);
}
};
const fetchUser = () => {
return fetch('/users/current')
.then((res) => {
if (res.status === 204) return null;
return res.json();
})
.then((user) => {
setUser(user);
return user;
});
};
useEffect(() => {
fetchUser().then((usr) => {
const cookies = parsedCookies();
if (!usr?.currency || !cookies.currency) chooseCurrency(JSON.parse(cookies.locale));
else if (usr?.currency) document.cookie.currency = usr.currency;
});
}, []);
//...
您还可以将货币存储在会话中。
Generally, you'd probably store the currency (and other user data) in a session store, and the cookie would identify the session.
Then the session store middleware would retrieve and annotate the request with the user data. That's the usual compromise; the middleware only knows about session state, it's not making business decisions.
Your application asks for request.session.currency