抱歉,用 Serverless 做这个标题主要是借势炒作。本文内容跟 Serverless 其实没半毛钱关系。
但是2X作者为啥要起这个名字博眼球呢?
众所周知,Serverless 的概念并不像字面意义上那样的 “无服务”,而是将中心化的服务端应用打散成为一个个函数式的服务,节约了前端编码到产品上线中间服务部署的操作成本。本质上是一种 云计算执行模型(Cloud Computing Execution Model)。
聪明的童鞋应该看完标题和开篇第一张图就猜到了我药里究竟卖的什么葫芦 —— 本文内容其实是关于 SSG(Static Site Generation,静态站点生成) 的一个解决方案。介绍一下鄙人最近是如何将一个 Mongo + Egg + React + Node 架构的动态站点(内部非开源版本图表库官网)降(mo)级(gai)为纯静态 SPA (开源图表库 Cloud Charts 的官网站点)并部署在 github pages 上的(注:考虑国内访问问题,目前托管在 gitee 和 netlify)。
为了凸显方案的独特之处,顺便为开源项目引引流,不免要先说明一下我所遇到的需求场景的复杂度和非典型性:
1. 需求介绍
1.1 背景
稍微介绍一下我们目前正在折(guan)腾(shui)的一个 Github 开源项目:阿里云 Cloud Charts —— 又一个开源的信息可视化图表库。
众所周知,这绝壁又双叒叕是一群无良技工在重复造轮子!(这里是一段辛辣的讽刺)。
一段不正经的补充说明:
- Cloud Charts 的前身是一套长期服务、应用于阿里云混合云场景的数据可视化解决方案(非开源版本),在日常工作中切实方便了很多不熟悉前端工作的研发童鞋进行业务实践
- 我们推出开源版本的其中一个目的,在于寻找更多适合沉淀的业务场景和更多志同道合的优秀的小伙伴 -(^_^)/ 没错~就是你!
1.2 需求
在维护 Cloud Charts 开源项目的同时,我们也需要将其内网版本的文档、示例、教程等资料同步搬运到公网,以方便公网用户对项目的了解和使用。
而对于 Cloud Charts 的非开源版本(TXD Widgets),我们已经实现了一套用于托管相关文档和数据的官网站点(集团内网)。这个内部站点是基于 MongoDB + Egg.js + React.js 形式开发和维护的,最重要的一个特点是依靠一个后台管理系统,方便开发图表库的童鞋能够及时维护和发布图表库项目的功能特性和对应的教程、示例、API文档等内容。
为了避免手动搬运内容、文档到外网所产生的费力且繁琐的工作,更为了能将站点的原生功能、特点尽可能透出给外网用户,我们希望将这套站点(前台页面功能和内容)部署到外网。
经过一番讨论和思虑,我们决定将原站点通过一系列工作流构建为一个 SPA 应用,方便快速发布和更新到 github pages 类型的静态页面服务中(真-Serverless ???)。
2. SSG 常规方案研究
针对两种主流的 SSG 模型进行分析,我们可以看到其各自的优缺点:
(1)基于构建时的 SSR(无头浏览器渲染后爬取静态内容)
优势
- SEO 友好
- FCP 友好(尤其当动态站点数据请求较大时)
不足
- 无法有效还原对前端路由功能
- 页面 “爬取” 任务耗时(尤其当动态站点的页面层级复杂时)
(2)XPress 类型(如 VuePress Next 等框架)
优势
- 开源框架较多、社区生态完备
- 提供部署到 github pages 等服务的能力
不足
- 与公司技术体系差异较大,需要框架迁移和大量代码重构
- 图表API & 在线Demo 等内容的输出是个超重的体力活
3. 另类方案的提出
内网站点的功能、复杂度如下图所示。
结合前面的相关分析,我们需要另辟蹊径,找到一条符合国情特色的发展道路。经过一定的业务抽象和一段时间的技术尝试,我们最终形成了一整套站点迁移的技术方案。
我们发现,动态站点要构建静态站点,最主要也是最重要的一环就是数据服务的静态化(即:数据接口的内容构建为可静态托管的前端资源)。
如上图所示,整个过程可以分为:数据静态化、静态数据加载、图片资源本地化、SPA 构建打包 共 4 个环节。下面的篇幅我们将逐个环节展开介绍,展现每个环节的具体内容、遇到的问题和相应解法。
4. 各环节姿势分解
4.1 数据资源的静态化
由于官网形态是比较结构化的内容页面,我们针对不同页面的特点、不同数据的关联关系,撰写了相应的浏览器脚本来模拟用户的点击、切换和跳转等行为。同时将 exceed 的数据请求记录下来(注:exceed 是一个 http 请求工具库,实质上是对 axios 的业务包装 - npm地址),经过一定处理后转存为本地数据(对敏感数据记录和字段进行过滤,将 “请求参数-数据返回” 以对应关系存储为本地 JSON 文件)。
静态化的数据格式形如:
// demo.json
[
// 单个请求的静态化数据
{
// 查询的条件(请求的参数)
params: {
api: 'fetchAll',
params: { module: 'studio' },
data: { page: 1, size: 0 },
},
// 查询的结果(请求的返回)
response: {
status: 0,
message: 'success',
data: [...],
}
},
...
]
4.2 加载静态数据
(1)页面挂载静态数据
SPA 页面加载时,先将静态化的数据加载到页面的 js 作用域中,便于数据查询方法的消费:
// 将静态数据解析为字典形式以方便查询(字典键值对的查询效率远高于列表)
function parseGlobalData(list) {
var globalData = {};
list.forEach(function (x) {
var key = objectToKey(x.params);
if (!globalData[key]) {
globalData[key] = x.response;
}
});
return globalData;
}
// 将请求参数(一个obj)转为字符串(base64),便于作为字典键值对中的键名
function objectToKey(obj) {
return btoa(JSON.stringify(Object.entries(obj).sort()));
}
// 将静态数据挂载到页面的 js 全局变量
function appendGlobalData(data) {
if (!window.staticGlobalData) {
window.staticGlobalData = {};
}
window.staticGlobalData = Object.assign(
{},
window.staticGlobalData,
parseGlobalData(data)
);
}
(2)改造 exceed.fetch 方法以消费全局数据
内网站点的数据是使用 exceed 类库请求后端接口进行异步加载的。在外网版本中,我们使用 Proxy 的方式重写了 exceed.fetch 方法以从本地读取数据(前面环节产生的静态化数据)。原理示意如下:
// 判断是否为外网站点
if (isStaticSite()) {
const handler = {
apply: (target, thisCtx, [params]) => loadLocalData(params),
};
// 代理 exceed.fetch 方法以消费全局变量中的数据
exceed.fetch = new Proxy(eceed.fetch, handler);
}
// 从全局变量中查询数据的方法
function loadLocalData(params) {
const key = objectToKey(params);
return window.staticGlobalData[key];
}
4.3 图片资源的本地化
在完成数据的本地化后,通过 nodejs 脚本实现可以匹配出官网所用到的全部图片资源(CDN 地址),对公网不能访问的图片资源(如内网语雀、OSS 服务上的资源)进行本地化转存。
4.4 SPA 构建打包
- 将内网站点的前端 router 改造为兼容 SPA 的形式
- 另起一个 SPA 项目用于构建公网版本
- 通过 nodejs 脚本实现将内网站点需要透出部分模块的源码 “一键” 同步到 SPA 工程中
- 通过 SPA 工程构建最终的线上环境代码
5. 结果展示
内、外版本的站点比较效果如下(仅方便展示部分首页内容):
可以看到,内网版本中图表库需要的重要功能模块都被原滋原味地还原、构建到外网站点中了(不可对外输出的部分在 SPA 构建时已经做了有效的过滤处理)。
6. 方案总结 & 改进方向
写在后面:本文主要介绍 SSG 的技术方案和一些关键环节的奇技淫巧。SSG 的过程中其实还有很多细节问题,如:如何在一套代码中维护内、外网不同版本的内容差异等。感兴趣的童鞋欢迎留言讨论。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。