构建国际化网站的完整技术架构:CMS改造、Nuxt路由与Lambda@Edge应用

本文详细介绍了upGrad.com网站国际化的完整技术实现。作者阐述了如何改造内部CMS(Apollo)以支持多地区页面管理,利用Nuxt.js的i18n模块处理客户端路由,并借助AWS Lambda@Edge根据用户地理位置实现自动重定向。文章深入探讨了后端数据模型变更、区域回退机制以及高可用架构的设计思路。

将upGrad.com推向国际

几个月前,upGrad决定拓展国际市场,因此需要在其面向客户的技术产品中支持这一扩张。首先需要进行重大改动的是课程列表网站——upgrad.com。在这篇文章中,我将介绍我们是如何规划和实现这一目标的,以及我们在过程中学到的经验。

国际化第一阶段的期望

国际化第一阶段对平台的期望是,内部团队应该能够在不同国家独立地推广upGrad的课程,即每个国家显示的内容可以不同,并且应该能够单独维护。此外,当访客尝试打开upgrad.com网站上的任何页面时,应自动重定向到其所在国家/地区的特定URL。这是许多网站的通用行为,它们通过在基础URL后附加特定代码来区分内容。

一个可能不那么显而易见的额外要求是在访客的整个会话过程中维护这个附加的代码。如果某人访问upgrad.com/us,那么所有后续的路由处理都应考虑初始的位置代码。例如,如果查看者点击导致路由跳转到/data-science-pgd-iiitb的超链接或按钮,那么最终的URL应该是upgrad.com/us/data-science-pgd-iiitb

总的来说,我们需要解决三个问题:

  1. 启用保存特定位置页面到数据库并允许分别编辑
  2. 处理客户端路由
  3. 根据用户位置自动重定向到这些特定页面

更新CMS以处理国际化页面

我们有一个内部内容管理系统,名为Apollo,用于管理upgrad.com上的内容。“页面”可以描述为数据库中映射到唯一“slug”的文档。以前,只能有单层slug,它们直接映射到upgrad.com上的URL。例如,data-science-pgd-iiitb映射到upgrad.com/data-science-pgd-iiitb。为了让页面拥有像upgrad.com/us/data-science-pgd-iiitb这样的两层URL,我们在页面文档中引入了一个名为i18n的新属性。这是一个对象,它包含locale的值,locale是分配给每个国家的唯一两位数代码。Locale是国家/地区很好的文本标识符,可以直接映射到URL。

在创建页面时,编辑者可以指定他们希望创建哪个国家的页面,创建后该值将附加到页面文档。我们将页面先前使用的slug标识符拆分为两部分,即namei18n.locale

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 旧的
{
  "slug": "data-science-pgd-iiitb",
  ...
}
// 新的
{
  "slug": "us/data-science-pgd-iiitb",
  "name": "data-science-pgd-iiitb",
  "i18n": {
    "locale": "us"
  }
}

name属性将包含页面所属课程的更符合逻辑的值。这可以用于以后将相同名称下的所有页面分组。如上文代码片段所述,slug属性现在包含了文档所代表的完整URL,也就是说,如果请求/data-science-pgd-iiitb的美国页面,我可以期望从上述文档获取数据。然而,页面数据加载逻辑并非如此简单,我稍后会谈到。

你可能会问,为什么我们要在nameslugi18n.locale中维护重复的信息。答案很简单——为了简化各种获取场景。在这个项目中,我们希望尽可能减少破坏性更改,并在可能的地方提供回退机制。我们内部称之为Apollo的CMS,以前仅供Web客户端使用。但在引入upGrad的移动应用程序后,它被扩展到了Android和iOS移动客户端。因此,保持CMS API端点和响应模式不变至关重要。

回退机制

为了能够覆盖尽可能多的地区,同时保持低数据占用空间,国际化营销页面采用了区域回退机制。如果按直接的方式思考,为每个课程的每个国家/地区创建一个页面意味着我们的数据量会呈m * n倍增长。这也意味着内容维护者将不得不更新多个页面,才能对特定课程的所有国家页面进行更改。这不是一个理想的方案。

为了解决这个问题,我们的想法是维护最少量的国际化页面,并用这少量页面的相关内容服务于全球所有地区。这意味着我们只需要维护少数页面,并且可以动态更新应为特定地区提供服务的页面。

概括来说,我们在API中引入了一个配置对象。这是一个JSON结构,它保存了按地区分组的位置信息。逻辑很简单,该树结构叶子节点的值表示各个国家/地区,这些国家/地区可能有与之关联的页面。如果没有,页面获取API将向上一级,并递归尝试获取与该地区关联的页面。

例如,如果客户端请求locale=ae的页面,我们会检查ae页面是否存在,如果不存在,则检查asia页面是否存在,再次如果没有,则返回所请求实体的全球页面。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 配置
{
  "global": {
    "asia": {
      "sg": {},
      "ae": {}
    },
    "europe": {
      "gb": {}
    }
  }
}

回退区域。印度页面仍然独立运行,全球页面定义了回退机制。

引入这个机制使我们能够将国家与特定区域关联起来,以便我们可以为属于该区域的每个国家提供区域数据。为了避免这个配置变得庞大,我们在自己的层面上确保每个页面都存在一个全球页面变体。这确保了如果请求某个国家(该国家不在我们的配置中)的页面,它总是会回退到全球页面。

页面端点现在接受一个locale参数,客户端可以通过它来指定需要哪个国家的页面。这主要涵盖了大部分的兼容性问题,特别是在逐步更新移动客户端时,因为端点保持不变,即使没有发送locale查询参数,API也默认假定为(印度),这是国际化之前的情况。

处理客户端路由

我们使用通用模式下的Nuxt来服务我们的营销页面。幸运的是,Nuxt(以及整个Vue社区)为常见用例提供了一套官方模块,包括国际化。多亏了nuxt/i18n模块,我们的很多工作都简化了。这提供了启动时自动生成静态路由、客户端路由,并提供了多种配置方式。

在很多地方,我们正在将用户重定向到应用内或应用外的URL。使用i18n后,这必须通过一个转换器函数来处理,该函数将当前的locale值附加到新的URL上。nuxt/i18n为此公开了一个名为localePath的函数。我们发现这是一个很好的机会,可以清理我们现有的重定向逻辑,并通过编写一个包装函数将其集中到一个地方,该函数用于重定向到URL,如果URL是内部的,则在内部可选地使用localePath

当应用启动时,nuxt/i18n从URL保存与当前locale相关的数据。例如,upgrad.com/us/data-science-pgd-iiitb将locale的值us(以及其他相关信息)设置到store中。利用这个,Nuxt应用会向CMS API发起调用,其locale查询参数等于模块设置的值。此后,API负责返回完全匹配的数据或应用一些回退机制来检索最接近的匹配数据。一旦数据返回,Nuxt应用会根据数据中指定的布局进行渲染。

自动重定向到国家/地区特定URL

这是整个项目中最具美感的功能。无论后台进行了多少数据库和API更改,看到URL神奇地更改为用户的位置代码都会让人感到惊讶。但作为开发人员,我们知道为了实现这一点,并确保平台其他部分像以前一样正常工作,付出了多少努力。

我们现有的营销网站通过CloudFront提供服务,通过启用边缘缓存实现低延迟。我们决定利用这个设置,为位置URL的路径增加一个额外的步骤。重申一下手头的问题:

当查看者(比如说,从美国)打开upgrad.com/data-science-pgd-iiitb时,请求应被重定向到upgrad.com/us/data-science-pgd-iiitb

这是按需执行函数的完美用例——AWS Lambda。AWS Lambda有一个名为Lambda@Edge的CloudFront变体,它创建函数的副本并将其部署到CloudFront分发网络中。然后可以将其配置为在某些触发器上执行。

Lambda@Edge可用的各种触发器。来源。

如上图所示,我们将Lambda函数放置在origin request触发器上。当CloudFront从源服务器请求内容时(函数位于CloudFront和源服务器之间),会触发此事件。Lambda的职责是创建重定向或将URL传递给源服务器。为了检测用户位置,CloudFront的国家头部信息派上了用场。除此之外,我们还使用了我们内部的位置解析API,该API由MaxMind的IP数据库提供支持。

要访问CloudFront的国家头部,需要将它们添加到行为配置中的白名单头部中。你可以通过名称CloudFront-Viewer-Country找到它。

用Node.js实现的Lambda函数大致如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
exports.handler = async (event, context, callback) => {
  let request = event.Records[0].cf.request;
  const uri = request.uri;
  const querystring = request.querystring;
  const countryCode = request.headers['cloudfront-viewer-country'][0].value;
  // 跳过一些可能不想重定向的URL
  if(shouldSkipRedirection(uri)) return callback(null, request);
    
  // 从头部或IP解析位置
  let { locale } = await resolveLocation(countryCode, request.clientIp);
    
  // 创建302重定向
  let response = {
    status: 302,
    statusDescription: 'Found',
    headers: {
      location: [{
        key: 'Location',
        value: '/' + locale + uri + (querystring ? `?${querystring}` : '')
      }]
    }
  }
  return callback(null, response);
}

一旦这个Lambda发布到边缘位置,origin request就开始流经这个lambda函数,从世界各地查看网站的用户看到了不同的内容。这是整个流程中的最后一步(用户旅程中的第一步),如果我们一步一步回溯,所有事情都各就各位。

请求流程图示(含Lambda@Edge)

当用户从美国打开upgrad.com网站时,lambda检测到位置并分配locale值us,然后在请求到达源服务器(Nuxt应用)之前重定向到/us。nuxt/i18n模块接管,将locale值设置为Nuxt上下文中的us,这导致触发API并附带相应的查询参数。API服务器尝试在数据库中查找美国页面,并将最接近的匹配文档返回给客户端。收到此数据后,客户端成功渲染页面并将其返回给CloudFront。然后CloudFront将此结果返回给查看者,并根据原始请求URL以及重定向后的URL对其进行缓存。就是这样!

未来展望

这是营销网站国际化的第一阶段,现在已经能够在多个国家推广我们的课程,并且我们能够在各地独立推送内容。接下来的步骤将包括添加本地语言支持并允许手动更改区域。除此之外,我们认为Lambda方法还有改进的空间。目前,跳过某些不符合此条件的URL的逻辑有点分散。我们正在考虑如何使其更加集中,以便可以通过单一的事实来源进行配置。如果你在你的应用程序中实现了类似的功能,请在评论中告诉我们,我们很乐意听取不同的观点。

如果没有使这个项目成功的整个团队,这个项目是不可能完成的。感谢Maitrey、Jitesh、Shahir、Abhishek、Harshita、Omkar、Akshay、Vishal、Yuvaraj及团队,以及所有做出贡献的人。请访问upGrad.com查看我们完全在线的课程!如果你希望与我们充满热情的团队合作,请查看我们的招聘页面。我们一直在寻找有抱负、有才华的人!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计