

{
"targets": [
"web"
],
"web": {
"ssr": true, // 通过调整 ssr 配置,让仓库应用发布 mpa 或 ssr 资源
"mpa": true
},
}


www.taobao.com/a.html
、
www.taobao.com/page/b.html
、
www.taobao.com/page/blog/index/c.html
进行访问,域名后标红的部分就是页面的路由。
-
页面源码目录:通常放在 src/pages/ 根目录下,较少嵌套情况; -
app.json 配置:app.json 里配置各页面的访问路由与对应页面资源; -
SSR render 函数目录:SSR 页面的渲染逻辑所在之处,位于 src/apis/render/ 下,可能有嵌套; -
render 函数中的 PAGE_NAME:render 函数需要使用的页面资源路径,服务端靠执行这个页面文件生成有内容的文档。
▐ 源码目录与路由配置
在多页应用中,不同页面的源码都写在 src/pages/
下的同级目录:
├── src
│ ├── app.json # 路由及页面配置
│ ├── components/ # 自定义业务组件
│ ├── apis/ # 服务端代码
│ └── pages # 页面源码目录
│ ├── a 页面
│ ├── b 页面
| └── c 页面
├── build.json # 工程配置
├── package.json
└── tsconfig.json
域名/a.html
访问,即使用页面在 pages 下的目录名(小写)。
{
"routes": [
{
"name": "myhome",
"source": "pages/Home/index"
},
{
"name": "pages/about",
"source": "pages/About/index"
}
]
}
source
指定页面的源码位置,name
指定页面路由,这样我们就能够通过 域名/myhome.html
、域名/pages/about.html
访问到页面。
构建结果的存放路径读取的是 name
配置:
└── build
└── web # csr 资源的构建结果放在 web 目录下
├── myhome.html/js/css
└── pages
└── about.html/js/css
▐ render 函数中的 PAGE_NAME
PAGE_NAME
是怎么用的:
// 页面名称,默认对应 pages 下的目录名
const PAGE_NAME = 'pages/index/index';
export default withController({
middleware: [
downgradeOnError(PAGE_NAME), // 降级中间件
],
}, async () => {
const ctx = useContext();
// nodejs 服务端的业务逻辑
// ……
// 生成渲染文档
const ssrRenderer = await useSSRRenderer(PAGE_NAME);
await ssrRenderer.renderWithContext(ctx);
});
PAGE_NAME
被传参给 useSSRRenderer
,以生成 SSR 文档。
从 useSSRRenderer
的源码中,可以看出 PAGE_NAME
是如何被消费的:
在函数内部,通过 PAGE_NAME
拼接出页面代码构建后的路径,然后从这个路径找出对应文件返回给 ssrRender
对象,最后执行生成一份文档。
那么页面代码构建后到底放在哪里呢?看一下 SSR 工程下的构建结果:
└── build
└── client # 客户端资源目录
| └── web # csr 资源依旧在 web 目录下
│ ├── myhome.html/js/css
| └── pages
| └── about.html/js/css
|
└── node # 服务端资源目录
├── myhome.js # node 端只生成 js 文件
└── pages
└── about.js
name
配置。
▐ render 函数的目录路径
└── src
└── apis # 客户端资源目录
└── render # csr 资源依旧在 web 目录下
├── myhome.ts
└── pages
└── child
└── about.ts
项目对应生成两条 SSR 链接:ssr域名/myhome
、ssr域名/pages/child/about
。可以看出,render 函数的目录路径就是它的访问路由。由于 SSR 链接访问的是一个服务,而不是一份文档资源,所以链接不是以 .html
结尾。
这里是为了便于理解才把 render 文件和 pages 目录的名字保持一致,根据对 PAGE_NAME 的介绍我们知道,服务端渲染使用哪个页面资源与 render 文件名无关。如果业务需要,你把它命名为 abcd 也没有关系。
所以,render 函数的目录路径几乎没什么限制,除了下面一种情况。
在本地启动的时候,应用会默认在浏览器打开生成的第一条链接,这里生成的链接由 app.json 决定。如果你的 render 对应目录路径里没有相应的资源,而浏览器又自动帮你打开了这个链接,服务端就会报错,甚至直接断开。所以 render 函数的最佳实践是与对应页面在 app.json 中的 name 配置保持一致。

-
pages 页面资源路径决定了 app.json 里的 source 配置 -
app.json 的 name 配置决定了构建后产物位置(CSR/SSR 产物都依赖这个值)、CSR 访问路由 -
render 函数文件的路径决定了 SSR 访问路由 -
render 函数中的 PAGE_NAME 决定了该函数使用的页面资源,需要与对应页面的 name 值配置保持一致

▐ 云构建问题
▐ 手动模拟代码执行环境
在测试预发 SSR 链接时,新的环境问题出现了。
定位了一下问题,发现是一个调试插件的锅。梳理一下它的执行逻辑,大概是这样:
window.__mito_result = 'something'; // 定义变量,挂载在 window 上
console.log(__mito_result); // 使用变量时,没有通过 window
not define
错。
-
修改插件执行时机 -
修改插件注入时机:因为服务端只需要生成文档内容,这个插件是文档无关的,不需要在预发资源里注入 -
环境模拟:使用框架能力对特定变量进行模拟
// 第一步、在 build.json 里配置 mockEnvBrowser
// build.json
{
"web": {
"ssr": {
"mockBrowserEnv": true
},
},
}
// 第二步、在 render 函数中进行传参
// src/apis/render/render-function-path.js
export default withController({
middleware: [
downgradeOnError(PAGE_NAME),
phaIntercept(PAGE_NAME),
],
}, async () => {
// 。。。
const ssrRenderer = await useSSRRenderer(PAGE_NAME, {
mockBrowserEnv: true, // 需要再次配置为 true
globalVariableNameList: [ // 待模拟的变量名列表
'__mito_data',
'__mito_result'
],
// 可以不对上面的变量进行实现,这时上面的变量在执行时值为 undefined
browserEnv: {}, // 待模拟变量的对应实现
});
// 。。。
}
这里顺便研究了一下框架侧的环境变量实现,还挺有意思的。在 useSSRRenderer 源码里,如果发现配置了 mockBrowserEnv: true
,会走到下面这个逻辑,其中最核心的是字符串构造的函数:
剥离一下它的执行核心逻辑:
// 定义一个执行函数
function mockEnvFn (...globalVariableNameList) { // 定义形参
execute('page.js');
// 页面处在 mockEnvFn 函数的上下文,页面逻辑中需要用到 window 的,可以从该函数的传参中取得
}
// 执行该函数
mockEvnFn(globalVariableList); // 传入实参
框架层给页面函数包了一个外层函数,为这个外层函数定义了形参列表,然后执行这个外层函数,这样页面函数就处于形参的上下文里,从而实现了环境模拟。
▐ 媒体/脚本标签注入
业务问题

__webpack_public_path__
实质上是 SSR 拿到运行时的配置,在访问时动态替换了资源链接。而 CSR 模式下,文档在构建结束后就生成了,没有办法在运行时动态更改资源链接。
为了测试同学能够正常工作,需要在访问网页时,通过资源代理的方式,对资源路径进行重定向。
▐ 商详链接替换问题

SSR 改造做到现在,各种分享做了不少。曾有其他团队的工程师问我,怎么你碰到的这些问题,我一个都没有碰到过?
我自己复盘后,结论是:原本为改造项目,需要带着镣铐跳舞。原项目已经做了一年多,本身复杂度比较高。此外,业务场景的多样性,使我们不得不同时维护 CSR 和 SSR 链接,这也是大多数 SSR 应用不必承担的责任。
这三点原因使我不得不去探索少有人走的道路,虽然曲折,好在走通了,这也要感谢架构团队大佬们的支持。
另外,也有一些别的体会。在推动一项技术在团队内落地的过程中,很大一部分因素不在技术层,而在于需求的定位、资源的协调、业务的落地。这些非技术问题如果能想在前面做在前面,对技术的落地只会大有裨益。

本文分享自微信公众号 - 大淘宝技术(AlibabaMTT)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。