写点什么

开发效率提升 50% 以上,爱奇艺官网主站的 Nuxt 实践

  • 2021-06-28
  • 本文字数:6487 字

    阅读完需:约 21 分钟

背景

让每一个用户获取到稳定、及时的页面体验,是前端工程师们一直以来努力的方向。


作为一个拥有丰富内容资源的视频网站,爱奇艺官网主站需要频繁进行节目上线或者下线、各种活动配置等操作调整,对于页面 SSR 服务的可用性及稳定性,都有着极高的要求。


2019 年之前,爱奇艺官网主站页面的 SSR 采用的是在 CMS 平台中书写 Velocity 模板,由 Java 编译,优点是渲染速度快但缺点也非常明显


(1)在 CMS 平台中开发体验不好:没有传统 IDE 方便,不能配置快捷键、不能安装插件等等,导致开发效率低下。


(2)前后端代码不同构:由于后端使用 Velocity 模板,而前端需要使用 Vue,导致前后端代码不同构。


(3)破坏 Vue 组件封装性:由于 Java 无法编译 Vue 组件,所有的 Vue 组件都需要用 Slot 的方式在 CMS 平台中书写以达到 SEO 和 SSR 的目的。


基于以上所有原因,我们决定使用 Node 来进行 SSR。因为我们的前端框架是 Vue,因此我们选择了配套的 Nuxt 框架进行 SSR


使用 Nuxt 进行 SSR,难点并不在于如何使用 Nuxt,而在于如何维护这个服务,保证其性能、稳定性等,因此,本文将不会介绍 Nuxt 的使用,其语法可以参考官网,这里将主要从性能、缓存、限流、灾备、日志等几个方面来介绍我们是如何保证 Nuxt 服务的可用性及稳定性的。

Nuxt 稳定性提升之路

2.1 页面配置

首先介绍一个很重要的配置文件。在我们的项目根目录下,创建了一个页面配置文件,用来存放每个页面的通用配置,例如页面的缓存配置、Purge 信息、主题色配置、广告信息配置等等,该文件导出一个 Object, 键值为页面的 Router Name,Value 值为页面的配置信息:


// configs/pageinfo.jsexport default { 'dianshiju-id': {...}, 'zongyi': {     theme: 'dark', // 页面主题色配置  }, 'home2020': {...}, 'rank-hot': {...}}
复制代码


然后我们在 Nuxt 插件中根据请求的路由信息,读取对应的页面配置,并将其注入到所有的组件实例中,方便随时取用:


// plugins/pageinfo.jsimport config from 'configs/pageinfo.js'export default ({ route }, inject) => {     inject('pageInfo', config[route.name]) // 注入页面配置信息}
复制代码


因此你可以在组件的任何地方获取到页面配置信息而不需要通过 Props 一层层传递,页面通用配置也不会散落在项目各个地方,方便统一管理。


<div :class="$pageInfo.theme">我是综艺页面</div>
复制代码

2.2 浏览器兼容性

虽然 Nuxt 理论上可以支持 IE9,但 IE9 在很多方面都需要添加 Polyfill,例如对 History API 的支持等,为了保持代码的简洁性,我们放弃了支持 IE9-,但我们依然在框架中保留了一套机制来支持 jQuery,使得高低版本可以共用 HTML,而不需要单独为低版本写模板,从而最大程度的减少兼容低版本浏览器的成本。


大致思路为,Nuxt 提供了一个’render.route’的钩子函数,该钩子函数的执行时机在生成 HTML 后,返回给用户之前。在这个钩子函数中,我们可以根据用户请求的 UA 信息判断用户版本,如果是低版本浏览器用户则移除 HTML 中高版本 JS 并注入低版本打包后的入口文件即可。


// nuxt.config.js    'render:route': (url, result, { req }) => {      if (isLowBrowser(req)) { // 根据用户ua信息判断是否是低版本        const $ = cheerio.load(result.html)       $('body script[src*=\'pcw/ssr\']').remove() // 移除高版本js        $('body').append('<script src="//stc.iqiyipic.com/jquery.js"></script') // 添加jquery        $('body').append('<script src="//stc.iqiyipic.com/index.js"></script') // 添加低版本入口js        result.html = $.html()      }
复制代码

2.3 性能优化

2.3.1 数据过滤

Nuxt 有一个很重要的机制在于,它会将所有 asyncData 函数返回的数据挂在`window.__NUXT__`上,通过 HTML 返回给客户端,从而避免客户端再次请求这些数据,因此,asyncData 函数中返回的数据量对性能的影响变得更加重要,它不仅影响了接口数据的传输时间,还影响了 HTML 的体积,因此,我们需要对这些数据进行压缩,在 NUXT 中,我们尝试了三种方案


1.在 asyncData 中做数据过滤

2. GraphQL

3. 数据过滤平台


在 asyncData 中做数据过滤仅减少了 HTML 体积,却并无法减少冗余数据的传输。


GraqhQL 虽然解决了冗余数据的传输问题,但代码不利于维护,因为它需要写大量的查询参数, 查询参数太长时还需要使用 POST。


// 非常不利于维护的查询字符串const query = `query {  qipuGetVideoBriefList (    album_id: "${params.album_id}"    type: "EPISODE_LIST"    play_platform: "PC_QIYI"    order: "DESC"  ) {    rpc_status    episode {      id      g_corner_mark_s      brief {        title        short_title        subtitle        page_url      }      release {        publish_time      }    }  }}`axios.get(`http://xxx.iqiyi.com/graphql?query=${query}`)
复制代码


最终,我们搭建了一个数据过滤平台,以可视化的方式来配置接口数据源、数据的字段过滤和映射,最终生成一个接口,该接口从配置的数据源获取数据,然后经过字段映射和字段过滤,仅仅返回我们需要的字段,这样既过滤了冗余数据又不需要维护 GraphQL 的查询参数,而是将 GraphQL 的查询串可视化为配置。


2.3.2  Layout

Nuxt 提供了 Layout 配置项,看似非常的方便,但通过分析 Nuxt 生成的.nuxt/App.js 入口文件,我们发现所有的 Layout 不管有没有被使用到,都会被打包进来,例如 A 页面使用了 LayoutA, B 页面使用了 LayoutB, C 页面使用了 LayoutC,则 A、B、C 三个页面的入口 JS 会有 LayoutA、LayoutB、LayoutC 的所有代码。


// .nuxt/App.jsimport _8daa19aa from '../src/layouts/a.vue'import _8daa19a8 from '../src/layouts/b.vue'import _8daa19a6 from '../src/layouts/c.vue'import _6f6c098b from './layouts/default.vue'
复制代码


因此,如果 Layout 的逻辑很复杂,并且如果代码量很大,所有页面的 JS 体积就会变大许多。基于以上原因,我们放弃了使用 Nuxt 的 Layout,而是自己封装了一个 I71Layout 组件来提供所有页面的通用功能,以减少冗余代码。

2.4 缓存

由于 Vue SSR 是基于虚拟 DOM,而 Java 是基于字符串,所以性能上相比之前会慢一些,因此我们从页面和组件两个粒度上做了缓存策略


我们使用 Nginx 反向代理来控制页面级别的缓存,默认每个页面缓存 5 分钟,当 Nuxt 返回非 200 时,Nginx 则使用过期缓存返回。


组件缓存我们使用的是官方的 @nuxtjs/component-cache 模块,它提供了一个 serverCacheKey 配置项,Nuxt 会以这个配置项的值作为缓存的 Key。因此我们为每个需要缓存的组件定义了一个 cache-key 的 Props, 传递后则会根据传递的值做缓存,未传递则无缓存。这样对于所有无缓存的页面在调用组件时,可以传递一个 cache-key 来使得组件被缓存,从而加速页面的 SSR。

2.5 purge

对于有缓存的页面,我们需要对应的 Purge 接口来清除页面缓存。页面的 Purge 分为两个部分一部分是我们 Nginx 反向代理的缓存 Purge, 另一部分是 CDN 缓存的 Purge,他们的 Purge 原理相同,因此这里我们只讲 Nuxt 服务的 Nginx 反向代理的缓存 Purge。


我们希望提供一个 Purge 接口,通过传递页面名参数来 Purge 指定的页面。我们的 Nuxt 框架本身是基于 Koa 搭建,所以我们只需要在 SSR 之前插入 koa-router,就可以提供我们的 Purge 接口。


// server/index.jsconst app = new Koa()const router = new Router()router.get('/api/purge/page/:pageName', async (ctx) => { // 定义purge接口,支持传递pageName  ctx.body = await purgePage(ctx) // purge nginx缓存和cdn缓存})app.use(router.routes()) // 插入我们需要的apiapp.use(ctx => { // nuxt 进行 ssr    nuxt.render(ctx.req, ctx.res)})
复制代码


那我们如何知道每个 pageName 要 Purge 哪些 URL 呢?这里我们需要在之前提到的页面配置文件中进行配置来将 pageName 和 Purge URL 关联起来


// configs/pageinfo.jszongyi: {   purge: {      purgeUrl: [        'https://zongyi.iqiyi.com/',        'https://www.iqiyi.com/zongyi'      ],   },}
复制代码


接下来我们只需要 Purge 所有服务上的这些 URL,服务部署在公司的应用平台,一共有 4 个集群,上百个 Docker 容器,我们需要 Purge 所有宿主机上的 Nginx 缓存,具体操作如下:


首先我们需要在 Nginx 中配置让其支持 Purge:


location / {    proxy_cache_purge PURGE from all;}
复制代码


这样就可以通过调用 http://{宿主机的域名}:{宿主机的端口}/purge/{uri}来 Purge 该宿主机上 uri 对应的缓存了。


接下来我们只需要逐个调用所有宿主机上的 Purge 接口就可以 Purge 所有的宿主机上的页面缓存了。

2.6 限流

对于无缓存页面,为了谨防恶意刷量行为,要进行限流。我们从 WAF,  单 IP 限流,  IP 黑名单进行了三方面的限制。

2.6.1 WAF(Web Application Firewall)

首先我们接入了公司的防火墙平台,通过智能识别以过滤掉一些恶意请求。其次,对于一些动态路由的页面,我们对请求的 URL 进行了正则匹配,不符合正则的请求全部拒绝访问并返回 403。

2.6.2  单 IP 限流

为了防止单 IP 脚本刷量,我们在 Nginx 反向代理使用 limit_req 模块进行单 IP 限流。对于普通用户和爬虫,我们设置了不同的访问频次,超过频次的请求拒绝访问并返回 503。

2.6.3 IP 黑名单

除此之外,我们通过日志分析会发现一些很明显的刷量 IP,对于这样的 IP,我们希望直接封禁。


如果直接在 Nginx 配置中添加 Deny 语句,会发现 Deny 并不会生效,是因为请求经过了网关,到我们的 Nginx 服务时,Remote Address 变成了网关的 IP,而我们 Deny 的是真实用户的 IP,所以我们需要想办法让 Nginx 知道用户的真实 IP 是什么。


通常用户的真实 IP 存储在 x-forwarded-for 字段中,为了拿到用户的真实 IP,我们需要在 Nginx 中做以下配置:


# nginx.confserver {    real_ip_header X-Forwarded-For; # 告诉Nginx,用户的真实IP存储在x-forwarded-for字段中    real_ip_recursive on;}
复制代码


但光有以上配置还不够,因为 x-forwarded-for 字段为一个字符串,每经过一个节点,这个节点就会向里面追加一个 IP,所以到达我们的 Nginx 时,该字段的值为 x-forwarded-for: {用户的真实 IP},{网关的 IP},而 Nginx 读取 IP 时,会默认从后往前读取 IP, 如果这个 IP 是受信任的 IP,则会继续往前读取,直到不被信任的 IP 就会当做是用户的真实 IP,因此,如果没有额外配置,Nginx 读取到的 IP 依然是网关的 IP,因此,我们还需要将所有网关 IP 添加到信任 IP 的列表中,Nginx 才能继续往前读取到用户的真实 IP。我们可以将整个内网网段都设置成信任 IP:


# nginx.confserver {     set_real_ip_from xxx.0.0.0/8; # 设置内网网段为信任IP    real_ip_header X-Forwarded-For; # 告诉Nginx,用户的真实IP存储在x-forwarded-for字段中    real_ip_recursive on;}
复制代码


现在 Nginx 可以读取到用户的真实 IP 了,这时候我们只需要创建一个 IP 黑名单即可:


# nginx.confserver {     set_real_ip_from xxx.0.0.0/8; # 设置内网网段为信任IP    real_ip_header X-Forwarded-For; # 告诉Nginx,用户的真实IP存储在x-forwarded-for字段中    real_ip_recursive on;    include ip-blacklist.conf # 导入IP黑名单}# ip-blacklist.confdeny xx.xx.xx.xx;
复制代码

2.7 灾备


对于无缓存的页面,除了限流以外,我们还需要有灾备方案,否则一旦服务出错返回非 200,用户将看到错误页面。


我们部署了一套独立的灾备服务,使用 Node 脚本每隔三分钟从线上服务拉取所有重要页面,如果页面返回 200,则将其存储为 HTML 文件,否则抛弃该页面,然后使用 Nginx 做反向代理来 Serve 灾备页面。


CDN 先从线上服务拉取页面,若返回非 200,则从灾备服务拉取对应的页面返回给用户,以此保证用户永远不会看见出错的页面。

2.8 服务端日志

服务端日志主要用来记录 Nuxt 渲染页面的记录、错误信息等,它们对于排查问题、统计流量来说是非常重要的,我们的服务端日志分为两大部分:页面渲染日志、接口请求日志。


页面渲染日志即每一次来一个页面请求,则写一条日志,记录页面的 URL、Referer、用户 Cookie、用户 IP 等信息,若页面渲染未出错写入到 logs/page/info.log 中,若页面渲染出错则写一条日志到 logs/page/error.log 中。


接口日志是每一次页面渲染中发出的请求日志,封装在底层发送请求的 HTTP 函数中,记录了调用该接口的页面 URL、接口 URL、接口参数等信息,若请求成功,则写一条日志到 logs/api/info.log, 若请求失败,则写一条日志到 logs/api/error.log 中。


// nuxt.config.jshooks: {   'render:setupMiddleware': app => { // 在nuxt初始化时插入一个中间件,每次请求都生成一个logParams对象      app.use(async (req, res, next) => {        req.logParams = {          requestId: generateRandomString(), // 生成requestId随机串          pageUrl: req.url        }        next()      })    },    'render:routeDone': (url, result, { req, res }) => { // 渲染完毕      logger.page.info({ type: 'render', ...req.logParams}, req) // 写日志时带上requestId    },    'render:errorMiddleware': app => app.use(async (error, req, res, next) => { // 渲染错误      logger.page.error({ type: 'render', error, ...req.logParams }, req) // 错误日志带上requestId      next(error)    }),}
复制代码


为了让页面渲染日志、这一次渲染的接口日志关联起来,我们会在渲染前生成一个唯一的 RequestId, 然后在该次渲染的所有日志中都带上这个 RequestId,就可以通过一个 RequestId 查询到页面渲染日志,以及这个页面发出去的所有请求日志了。


// http.js class Resource {  async http (opts)    let data    try {        data = await axios(opts)        process.server && logger.api.info(opts, this.req.logParams) // api日志带上requestid     } catch (error) {         process.server && logger.api.error(opts, error, this.req.logParams) // api错误日志带上requestid     }     return data  }}
复制代码

2.9 日志采集

我们采用了 Filebeat + Elasticsearch + Kibana 进行日志管理首先通过 Filebeat 进行实时日志采集,然后上报至指定 kafka 集群,然后对日志进行分析并建立索引,最终生成一个可视化的日志查询页面,这样我们就可以查看一段时间内符合查询条件的日志了。


2.10 流量监控

基于服务端日志,就可以据此统计流量经由了 CDN 的缓存、WAF 的拦截、Nginx 反向代理的缓存,最后计算出到达我们的 Nuxt 服务的实际流量到底有多少。我们可以根据日志的 time 字段筛选出指定时间段且 type= 'render'的日志,就是该时间段内 Nuxt 服务承受的总流量了,如果想看各个页面的流量,还可以进一步对日志中的 pageUrl 字段进行筛选。


总结

Nuxt 从根本上解决了之前在 CMS 平台使用 Velocity 开发遇到的所有问题,但同时也带来了一些别的问题,例如域名冲突的问题、服务端变量共享的问题、渲染性能问题等。不过总体来说,瑕不掩瑜,开发体验得到了质的提升开发效率提升了 50%以上组件复用率更高、组件封装性更好,代码可读性可维护性都得到了飞跃性的提升;在 CDN 缓存、Nginx 反向代理缓存、组件缓存的强力加持下,页面的渲染性能也并没有下降;由于移除了一些由于前后端代码不一致、大量使用 Slot 等一些复杂逻辑后,首屏渲染性能反而提高了许多,服务器响应时间维持在平均 0.5s 左右,错误率维持在 0.2%左右,而在有灾备服务兜底的情况下,可访问性也几乎达到 100%


最后,期待 Nuxt3 的到来以及性能和开发体验上的进一步提升。


本文转载自:爱奇艺技术产品团队(ID:iQIYI-TP)

原文链接:开发效率提升50%以上,爱奇艺官网主站的Nuxt实践

2021-06-28 14:005715

评论 1 条评论

发布
用户头像
有一个问题,假设页面和组件设置相同的缓存的时间,那不是新的request只会命中页面缓存,组件缓存是否没有用了呢

由于 Vue SSR 是基于虚拟 DOM,而 Java 是基于字符串,所以性能上相比之前会慢一些,因此我们从页面和组件两个粒度上做了缓存策略。

2021-07-13 17:39
回复
没有更多了
发现更多内容

RocketMQ和Kafka的差异对比

编程江湖

大数据

Linux一学就会之Linux计划任务与日志的管理

学神来啦

Linux centos 运维 SSH linux云计算

开发者故事|朝九晚六大小周,我就是快乐的技术人

尔达Erda

程序员 云原生 经验分享 成长笔记 思考路径

黑客进行网络欺骗攻击的手段有哪些?

喀拉峻

黑客 网络安全 安全

CSS之选择器(七):empty

Augus

CSS 12月日更

百分点大数据技术团队:Elasticsearch多数据中心大规模集群的实战经验

百分点科技技术团队

利用Java反射处理private变量

FunTester

Java 工具 反射 Groovy FunTester

哪些企业需要做等保测评?快速过等保测评用什么堡垒机好呢?

行云管家

网络安全 等保 等保测评 等保2.0

低代码如何让中小型企业蓬勃发展?

低代码小观

低代码 企业管理系统 低代码平台 中小企业

公安合成作战平台建设解决方案,情指勤一体化合成作战系统搭建

电微13828808271

使用kubernetes,你真的降本了吗

Geek_cd6rkj

Docker Kubernetes 云原生 成本治理 提效降本

TDengine典型用户案例|内容合集

TDengine

tdengine 时序数据库 技术专题合集

质量基础设施一站式服务平台建设,检验检测系统平台开发搭建

电微13828808271

公安情报研判分析系统开发,情指勤一体化实战平台

电微13828808271

「Spark从精通到重新入门(二)」Spark中不可不知的动态资源分配

尔达Erda

大数据 spark 程序员 云原生 心得

西南林业大学:用宜搭打造智慧校园,节省百万费用

一只大光圈

钉钉 低代码 数字化 高校 钉钉宜搭

基于社交网络的客户智能 ( AI ) 推荐系统研究

索信达控股

推荐系统 推荐算法 个性化推荐

常见序列化算法学习笔记一

风翱

序列化 12月日更

14. 《重学JAVA》-- 内部类

杨鹏Geek

Java 25 周年 28天写作 12月日更

RESAR 性能工程:一个性能项目真正体现价值的方式

zuozewei

内容合集 签约计划第二季

自身的强大,无惧任何威胁!

Tiger

28天写作

【实战】网络安全学习:内网渗透实例

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 内网渗透

英特尔深耕元宇宙算力技术,剑指开“元”盛世

科技新消息

Rainbond 5.5 发布,支持Istio和扩展第三方Service Mesh框架

北京好雨科技有限公司

istio PaaS rainbond

埃文科技荣获“郑州市企业技术中心”

郑州埃文科技

埃文科技 ip技术 企业技术中心

从0开始学VUE - 运行第一个VUE项目

恒生LIGHT云社区

JavaScript node.js Vue 前端

聚势聚能,共擎共飞 PKS安全先进计算2021生态大会将启

InfoQ 天津

云原生监控高可用集群 Thanos 架构剖析 | 内容合集

耳东@Erdong

内容合集 签约计划第二季

【量化】用数据验证巴菲特推荐的指数定投的收益真相

恒生LIGHT云社区

数据 金融科技 量化投资 量化

DotNet项目利用拦截器记录访问日志

为自己带盐

日志 dotnet 28天写作 12月日更

企业数字化的4个痛点,低代码平台如何解决?

J2PaaS低代码平台

低代码 数字化 低代码平台

开发效率提升50%以上,爱奇艺官网主站的Nuxt实践_大前端_爱奇艺技术产品团队_InfoQ精选文章