2020年5月22日FOLLOWME5.0的第一个版本终于上线了,这也是公司内部基于 Genesis
上线的第二个项目。首页是老项目经历了最原始的那种 Vue SSR,后来在年初的时候,迁移到了 Nuxt.js 下,再到现在迁移到了 Genesis
,可谓是一波三折。
首次实践
在2019年的上半年,我们在和 APP 混合开发的项目中,首次实践了模块化,它拥有了独立的 API、路由、状态和页面,并且是按需进行初始化的,但是它并不是完美的,它是基于路由的微模块,并且状态也是按需注入到全局的状态中的管理
无路由微模块
2019年的8月份,我开始介入到 web 端首页的开发,首先面对的就是基于 Vuex 的状态管理,所有的状态全部注入到全局的状态,交集到一起,随着业务的迭代,已经分不清楚哪些是需要的,哪些是可以被删除的,随着业务规模的膨胀,越来越难以维护。为了保证无入侵性,基于Tms.js抽象出来了一个支持微模块的状态库,这时候我们的微模块拥有了独立的 API、状态和页面。
我们5.0左导航就被抽离成一个独立运行的微模块,一个微模块由多个组件组合而成,它有自己内部状态管理。
大跃进
在2019年年底的时候,得知我们要升级5.0的版本,网站也将会大幅度的重构,在基于已有的微模块开发理念下,我们希望能够更近一步,解决一些以往项目架构的弊端。
5.0之前,我们有一个公共的导航栏,不管是 CSR、还是 SSR 的项目都需要它,每次导航栏发生变更之后,都需要重新打包十几个项目发布,这极大的消耗了我们的身心体力,我希望能做到一个页面可以由不同的 SSR 服务聚合而成,通过 API 的形式提供给另外一个服务使用。
这个大胆的想法诞生后,我们深入的研究了一下 Nuxt.js,期望它可以做到我们的需求,经过一番调研后,确认无法满足我的需求后,最终还是选择了造轮子。
Genesis
参考了一些社区 SSR 框架的实现,基本上都是封装好的框架,灵活性较低,而我们期望它是一个简单的 SSR 库,当成一个工具函数来使用,以便于能够支撑多实例运行。
在这里的理念下, 它没有像 Nuxt.js nuxt.config.js
这样直接读取一个配置开始运行,给你集成了各种各样的功能,它仅仅一个基础到不能再基础的渲染工具函数
import { SSR } from '@fmfe/genesis-core';
const ssr = new SSR();
const renderer = ssr.createRenderer();
renderer.render({ url: '/' });
当然了,在实际的业务中,我们还需要创建一个 HTTP 服务将我们的内容返回给用户。
如果要做到微服务,并且能被不同的服务之间调用,首先就需要服务的自身具备将渲染结果输出 JSON
的能力,然后第三方服务读取渲染结果,输出到 HTML 中
renderer.render({ url: '/', mode: 'ssr-json' });
我们巧妙的使用了 Vue 的 Renderer
选项 template,传入一个函数,执行完成后返回了一个 JSON 的渲染结果
const template: any = async (
strHtml: string,
ctx: Genesis.RenderContext
): Promise<Genesis.RenderData> => {
const html = strHtml.replace(
/^(<[A-z]([A-z]|[0-9])+)/,
`$1 ${this._createRootNodeAttr(ctx)}`
);
const vueCtx: any = ctx;
const resource = vueCtx.getPreloadFiles().map(
(item): Genesis.RenderContextResource => {
return {
file: `${this.ssr.publicPath}${item.file}`,
extension: item.extension
};
}
);
const { data } = ctx;
if (html === '<!---->') {
data.html += `<div ${this._createRootNodeAttr(ctx)}></div>`;
} else {
data.html += html;
}
data.script += vueCtx.renderScripts();
data.style += vueCtx.renderStyles();
data.resource = [...data.resource, ...resource];
(ctx as any)._subs.forEach((fn: Function) => fn(ctx));
(ctx as any)._subs = [];
return ctx.data;
};
远程组件
考虑到不是所有的项目,都需要远程调用,所以远程调用组件,我们是以独立的包提供的。在拿到 SSR JSON 的渲染结果后,远程组件将会帮我们负责嵌入到该服务的页面中去。
<template>
<remote-view :fetch="fetch" />
</template>
fetch
是一个异步的回调函数,你可以通过使用 axios 库来发送请求将 SSR 渲染的结果返回给 remote-view
组件。
renderer.render({ url: '/', mode: 'ssr-json' }).then((r) => {
// 编写一个接口, 将 r.data 提供给 remote-view 组件访问
});
5.0的服务拆分
根据 UI 的呈现效果,我们将左导航和内容区拆分成不同的服务,其中内容区因为开发团队的不同、业务的不同,又拆分成不同的服务。
第一个版本我们拆分了左导航的
node-ssr-general
服务,首页和通知的 node-ssr-home
服务,以及信号的 node-ssr-signal
服务,每一个服务都是独立部署、独立开发、由不同的人进行维护,只是最终由 node-ssr-general
服务进行聚合。
未来我们的产品还将会进一步迭代,越来越多的服务都将会被集成进去这个大应用中。
内网域名
最初的 SSR 服务,尝试过使用 RPC 和 HTTP 获取接口数据进行渲染,后面经过权衡后,在5.0我们统一采用了在服务器配置 HOST 的方式,配置一个内网的域名,提供给 SSR 服务通过 HTTP 请求来获取数据
服务的加载策略
为了首屏可以更快的呈现给用户,所以在服务端远程组件走的是 SSR 的渲染,在客户端路由切换的时候,走 CSR 渲染,所以在首页
切换到信号
栏目时,会有一些加载过程明显的白屏。
后续可以通过 Service Worker 对所有服务的静态资源和 CSR 渲染时的 HTML 进行预加载,减少加载该服务内容时,出现白屏的几率
关于微前端
从理论上,Genesis
也可以同时做到 React 的支持,输出同样标准的 JSON 渲染结果、同样标准的应用创建和销毁逻辑。只是目前我们团队都是以 Vue 为主,所以这方面还需要团队有时间才能支持。