首页 > 解决方案 > 如果 `accept-language` 标头中没有国家代码,何时询问客户他们的首选货币?

问题描述

我的代码在做什么:确定客户货币

我从accept-language标题中获取客户首选的语言环境。从accept-language标题中,我获得了语言和国家/地区代码,以帮助我确定他们的首选货币。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);

在另一个中间件中,我使用国家代码来确定货币。

问题

但有时用户可能在标题中只有一个语言代码,而没有国家代码——比如en英语。您需要国家来确定货币。所以你会怎么做?

在这种情况下,您要么必须

我会选择最后两个中的任何一个。但是我很难弄清楚我什么时候做这些。

如果这是一条路线,我能做什么

如果 cookie 是通过某种路径设置的,/setCookie那么这将很容易:响应可以向客户端指定下一步是什么。例如,服务器可以使用 JSON 对象发送 200 状态,例如{stillNeedCountry: true}. 然后客户可以知道需要采取更多步骤。

但这是一个通用的中间件

但是 cookie 通常不会在特定的路由请求中设置。它们是在从该客户端向服务器发出的任何第一个请求上设置的,中间件在每个请求上调用。这让我很困惑。我们可以在中间件中检测到没有 countryCode,但那又如何呢?

解决方案?

我是否劫持了请求并直接从中间件发送响应,告诉前端该怎么做?这看起来很复杂,因为我们必须在前端设置每个 fetch 请求来处理这个响应。

有什么可能的解决方案?

标签: node.jsrestexpresscookiesapi-design

解决方案


我在这篇文章中的假设是不正确的。

与安迪·帕尔默的谈话让我明白了很多。

  • 我认为中间件是传递请求的任何函数,无论它是否仅在某些端点上。但事实证明,中间件是所有请求都经过的功能。我称之为“通用中间件”。
  • 我认为 cookie 需要始终以某种“中间件魔术”设置在后端。他们不。它们可以使用特定的路由请求和在前端进行设置。
  • 我误解了前端、后端和中间件的工作。

我认为 cookie 需要始终设置在后端,因为我对 cookie 的唯一其他体验是使用express-session后端中间件中设置 cookie 的位置。这只是我根据以前使用 cookie 的方式做出的假设。

您不要在中间件中设置 cookie,因为它是应用程序/业务逻辑问题,而不是基础架构问题
……
货币选择是用户选择,因此由用户设置。
...
使用中间件使用应用程序不需要做的基础设施事情来注释请求。
...
可以使用中间件使用所选货币来注释请求,但感觉有点特定于应用程序。

例如,从接受语言标头中提取国家和语言的中间件

-安迪

可以使用特定的路由来查询是否设置了 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;
    });
  }, []);
//...

您还可以将货币存储在会话中。

通常,您可能会将货币(和其他用户数据)存储在会话存储中,并且 cookie 将识别会话。

然后会话存储中间件将使用用户数据检索和注释请求。这是通常的妥协。中间件只知道会话状态,它不做业务决策。

您的应用程序要求 request.session.currency


推荐阅读