关注“之家技术”,获取更多技术干货
总篇150篇 2022年第25篇
★ 目录 ★
01 | 前言 |
02 | 基础概念 |
03 | 接口层的缓存方案 3.1 细化场景 3.2 实现路径 |
04 | 请求耗时直观化 4.1 直观的ServerTiming 4.2 实现路径 |
05 | 结语 |
汽车之家用户产品中心的前端团队,将 SSR 同构技术应用在 PC & M 主站中。相对于原有技术方案,在页面渲染性能、白屏时间、可维护性、用户体验都有大幅度的提升。我们结合公司技术基础设施和自身特点,经过长时间的升级与优化,逐渐沉淀出了一套最佳实践。本文即介绍其中的两个技术环节,以飨读者,分别是:
接口层的缓存技术方案 - 如何在应用中使用缓存优化接口性能
请求耗时可视化的实现 - 如何使用浏览器方便的看到接口请求耗时
随着前端复杂度的不断升高,浏览器能力被无限放大和利用,伴随着 NodeJS 的强势崛起,各种应用框架的层出不穷,从 CSR 到 SSR,从 SSR 再到同构。放眼当下,在不同的业务场景中,充斥着它们不同的身影。本次分享的开篇,我们先从以下几种常见的渲染模式讲起:
本篇文章,我们着重将目光聚焦于源站中的一环 - 接口 API 缓存层(上图中红色图标部分),在整个缓存链路中,API 接口缓存也极为重要:因为它可以保障接口查询耗时稳定高效,不因程序复杂或数据庞大而产生耗时波动;同时,它还可以在源接口发生故障时,通过返回历史数据的方式提供容灾能力,减少由此带来的影响,从而提升终端用户使用体验。
3.1 细化场景
通过实际的调研分析,我们整理了一些缓存的应用场景,并对其进行了一一实现:
cache-control
字段,作为缓存有效期除了以上的一些核心功能,对于基础的 TTL 有效期、缓存 Key 生成规则、日志输出等,均已内置支持。
Axios 是一个非常优秀而又被广泛使用的请求库,我们选择其的原因还在于它天然对 Node 端和浏览器端的双重支持。
那么,它是如何实现对 Node 端和浏览器端的双重支持的呢?那就是,内置适配器 - adapter;
我们先通过一张图来了解适配器在 Axios 中扮演的角色:
从图上可以看出,若想实现对接口请求的拦截和响应,大致有两种方式:
在实际业务场景中,拦截器往往是用于处理业务相关的通用数据逻辑,若想由此实现该功能,需要在请求拦截器和响应拦截器中,分别加入相关代码,对业务逻辑有一定的侵入性,集成方式繁琐,不够灵活,我们更期望的是一种零侵入、插拔式的集成方案。
因此,我们实现了一个自定义适配器,在不改变现有业务代码前提下,通过一行代码即可实现开启 / 关闭全应用的数据缓存。
如果你对适配器还不甚了解,可以先移步官方文档进行进一步了解。下面是一个自定义适配器的最简单的示例:
module.exports = function coustomAdapter(config) {
return new Promise(function dispatchRequest(resolve, reject) {
// ...other code
}
}
为了方便集成到项目中,我们发布并提供了一个 Npm 包,并向外暴露出了配置器;除此之外,结合之家的接口数据规范,还对 Axios 进行了业务上的封装,可以更方便的应用到日常的业务开发中。
import { installCache, installRequest } from '@ace/request';
const { adapter } = installCache({
ttl: 3 * 1000,
// ...other cache 参数
});
// 通过以下方式,可以单独将适配器集成到已有项目中
const reqIns = axios.create({
baseURL: 'https://api.autohome.com.cn',
// 自定义 adapter
adapter,
// other options
});
// ...other code
以往的方式,如果我们想了解接口性能,只能在应用中打日志来记录相关指标数据,而该数据的获取相对繁琐且滞后。为了解决这个问题,可以用浏览器直观快速的获取,我们引入了 ServerTiming(参考链接见文末)。
当 adapter 被集成使用后,为了能够实时分析 SSR 中接口请求的耗时情况,为此,我们开发了一个工具 - ServerTiming-Loader。
顾名思义,它是一个 Webpack Loader,通过它,便可以在不侵入业务代码的前提下,由构建层前置将耗时统计代码注入到业务代码中,为页面请求追加 ServerTiming 响应头,并在浏览器的开发者面板中实时体现出来。
如下图所示,以某次页面加载为例,页面首屏接口耗时为 11ms,使用这个时间指标,往往在 FCP、LCP、TTFB 等性能分析时,非常具有参考意义。
得益于 Next.js 框架本身的一些强约束,在 Webpack 编译过程中,我们可以很方便而又准确的定位到哪些模块属于页面级源文件,同时在页面源文件中能够通过静态分析得出 getServerSideProps
方法代码段。参考 Babel 的核心三步的工作流程(如下图),我们修改了其中的内部实现:通过 Babel 对 AST 解析及转换的能力,将 getServerSideProps
包装为高阶函数,并在函数内部追加上耗时统计代码。
Babel 工作流程:Parse(解析源文件)-> Transfrom(转换)-> Generator(生成新文件)
最终,我们将会生成如下模板代码:
/**
* 定义新的 getServerSideProps,并调用 API 执行
*/
const getServerPropsTempl = astTempl(
`
/**
* 追加 getServerSideProps 耗时到 Chrome Tools / Timing
*/
const getPropsWithTimingLoader = async ( cxt ) => {
const startTime = Date.now();
const returnProps = await originalGetServerSideProps(cxt);
const { res } = cxt;
setServerTimingHeader(res, {
key: 'API',
value: \`dur=\${Date.now() - startTime}\`,
});
return returnProps;
};
export const getServerSideProps = getPropsWithTimingLoader;
`,
{ placeholderPattern: false }
);
在 AST 的遍历过程中,我们还需要识别出 getServerSideProps 的不同声明形式,比如:
export const getServerSideProps = async () => {
// ...code
}
或
export async function getServerSideProps (){
// ...code
}
对于无法匹配或者匹配失败的语法类型,将原代码进行返回,保证页面功能的正常执行。
astTraverse(pageAST, {
/**
* 去除掉 getServerSideProps 的模块导出
* 示例:
* --------------------------------------------------------------------------------------
* 转换前:export const getServerSideProps = async () => { ... }
* 转换后:const getServerSideProps = async () => { ... }
* ---------------------------------------------------------------------------------------
* 转换前:export async function getServerSideProps (){ ... }
* 转换后:async function getServerSideProps (){ ... }
*/
ExportNamedDeclaration(path) {
// ...codes
},
/**
* 将原 getServerSideProps 方法重命名为 originalGetServerSideProps
* 当前为字面量形式声明
*/
VariableDeclarator(path) {
// ...codes
},
/**
* 将原 getServerSideProps 方法重命名为 originalGetServerSideProps
* 当前为 function name 形式
*/
Identifier(path) {
// ...codes
},
});
在 SSR 项目中,接口请求可能出现在 Node 和 浏览器等不同的宿主环境中,借助 Axios 请求库的一些特性,我们对其进行了一些封装,通过不同的缓存介质,实现了请求劫持、数据缓存和精细化场景的缓存配置,在 Server 端,通过 Redis 还可以实现多实例的缓存数据共享;同时,通过在构建层的一系列预处理操作,在 SSR 应用的运行时,通过 Server Timing 还可实现追踪页面 Server 端实时的请求耗时,并体现到浏览器的开发者面板中。
至此,API 接口缓存技术层在 SSR (请求)中的应用,我们已经介绍完了,在日常的开发中,我们可以视业务场景进行选择性开启,针对 CSR / SSR 不同项目类型上的调用方式也并无差异;当然,以上内容只是 SSR 中应用缓存链路的某一个环节,在 Render 层与更前置的 HTTP Server 层,我们有着更丰富的缓存技术方案在应用,期待下一次的分享。
汽车之家
米梦宇
用户产品中心-App技术部
2019 年加入汽车之家,目前任职于用户产品中心-App技术部-前端开发团队-产品库前端开发组。
主要参与汽车之家产品库相关前端开发工作。
阅读更多:
▼ 关注「之家技术」,获取更多技术干货 ▼