背景
之前我开发了一些工具,每个页面是一个html
文件,整体是个多页面应用。包括这些:
- 备忘录: https://tool.hullqin.cn/memo.html
- URL解析: https://tool.hullqin.cn/url-parser.html
- 字节解析: https://tool.hullqin.cn/byte-parser.html
- ProtoBuf解析: https://tool.hullqin.cn/pb-parser.html
- JSON格式化: https://tool.hullqin.cn/json-formatter.html
- 表格转换: https://tool.hullqin.cn/table-converter.html
- 换行符清除: https://tool.hullqin.cn/line-break-remover.html
- 颜色混合: https://tool.hullqin.cn/color-mixer.html
- 图片裁剪: https://tool.hullqin.cn/img-editor.html
当时,每一个工具都有一个URL,每个页面只有本工具的内容,没有统一的「导航栏」,这对于工具网站是非常不方便的。所以,我需要加一个统一的导航栏,方便用户在多个页面之间跳转。
我做事情很谨慎,一定要罗列多个方案,再做决策。
我把所有可行的方案都罗列到了本文中,并描述了各个方案的优点、缺点。方便大家遇到相同问题时做决定。
导航栏特点
罗列方案前,你需要知道:
- 导航栏是可变的,每当你新做一个页面、修改某页面的标题或URL,都需要更新导航栏。
- 所有页面的导航栏,应该具有一致性,更新时要统一更新(否则用户会比较困惑)。
方案一:服务端渲染
这里服务端渲染主要包括2种:
- 基于NodeJS框架做的SSR。
- 基于其它后端框架模版做的动态渲染。
他们都可以实现这种的效果:用户请求某个页面的html时,后端动态拼接好一份完整的html,返回给前端。在拼接过程中,把导航栏的html片段加进去。
优点
白屏时间短,SEO好。
缺点
- 服务端渲染是需要耗费服务端资源的,即使渲染结果可以缓存,我依然不建议浪费这些计算、存储资源。
- 服务端需要维护好导航html片段。而服务端代码和前端代码通常不在一个仓库,如果开发者手动更新导航html片段,效率低,容易忘记。即使你做了自动化方式同步,这也涉及到跨仓库同步,不是很方便。
- 开发过程中,为了达到跟线上一样的效果,可能还需要启动后端服务,导致本地开发测试不方便。
综上,如果你的网站本身没有服务端渲染,我不建议你仅仅为了增加导航栏而采用该方案。
方案二:前端编译时插入
前端增加编译环节,源代码不写导航栏,编译后,自动在特定位置插入导航栏的html片段。
优点
- 白屏时间短,SEO好。
- 可以放在CDN。
特点
- 需要增加编译环节,可以借助Webpack等工具。
如果不想使用Webpack,也可以像我一样,手写编译脚本(基于NodeJS):
首先是build.js
,它遍历src
文件夹下的html文件,针对每个html文件,跑一遍函数addNavigation
,把结果写入build
文件夹。
// build.js
const fs = require('fs');
const addNavigation = require('./navigation');
fs.readdirSync('../src').forEach(filename => {
if (!filename.endsWith('.html')) return;
const html = fs.readFileSync('../src/' filename, 'utf-8');
const newHtml = addNavigation(html, filename);
fs.writeFileSync('../build/' filename, newHtml, 'utf-8');
});
然后是navigation.js
,它就是针对html源代码做修改,返回新的html片段,已经插入了导航栏html片段。
// navigation.js
const config = [
{name: '备忘录', url: 'memo.html'},
// ...
];
const getNavigationHtml = (filename) => {
return '<div>导航栏html片段</div>';
};
const addNavigation = (html, filename) => {
let newHtml = html;
const navigationHtml = getNavigationHtml(filename);
const bodyIndex = newHtml.indexOf('<body>') 6;
newHtml = newHtml.substr(0, bodyIndex) navigationHtml newHtml.substr(bodyIndex);
return newHtml;
};
module.exports = addNavigation;
为什么这么设计呢?因为addNavigation
只是编译的一个环节,之后可以方便的增加addHeader
、addFooter
等等。
缺点
- 每次更新导航栏,需要重新编译所有项目,并重新发布所有页面的html文件。(但它是可接受的,全部重新编译、全部重新发布,完全可以自动化实现,且成本很低)
我个人就是选择了这种方案,参考: github.com/HullQin/tool-hullqin-cn/tree/main/scripts
方案三:前端运行时插入(UMD、模块联邦)
通过script动态引入导航js,运行时插入html片段(即UMD方式,Webpack的模块联邦也属于这种方案)。
为什么必须通过script引入?
因为导航栏的一致性和可变性,开发时它一定是只存了一份代码的。因为本方案不在编译时统一插入,而是在运行时动态插入,所以就需要多个页面引入同一份js文件,动态插入一样的导航栏。
优点
解决了方案二的缺点,每次变更导航栏,只需要重新发布script即可,不需要重新发布其他工具的html。
缺点
- 加载速度较慢,可能存在导航栏闪动问题(因为script是异步加载的,展示页面内容时,可能还没下载好导航栏对应script)。
- SEO不好。
- JS缓存时间不能太久。如果缓存太久导致无法及时自动更新、如果缓存太短导致经常加载速度慢。
如果可以接受这些缺点,这确实是非常好的方案。适合内部平台使用。
方案四:基于框架组件
如果页面整体是同一个项目,同一个框架,那么使用组件是最方便的。
这时候基本不需要决策了,直接无脑用组件吧。
方案五:基于微前端
微前端的初衷正是为了解决巨石应用,也可以让多个应用放到同一个SPA中,切换更流畅。
微前端方案中,通常分为「主应用」和「子应用」。可以把导航栏放在「主应用」中。
优点
- 框架不受限制。
- 可以让多页面应用(MPA)体验像单页面应用(SPA)一样(即切换页面时,导航栏不闪烁)。
缺点
- 重。
如果你的项目本身不是基于微前端的,没有必要为了加导航栏而引入微前端方案。
你可以看看我的网站 tool.hullqin.cn,它没有采用微前端方案,本身是个多页面应用(非SPA)。但因为浏览器有缓存,所以体验非常丝滑,在多个页面之间切换非常快。
方案汇总
方案 | 框架限制 | 首屏加载速度 | SEO | 可维护性 |
---|---|---|---|---|
服务端渲染(SSR或模板渲染),统一在html特定位置插入导航html片段 | 无 | 较快 | 很好 | 导航html片段在后端项目,需维护好它 |
前端编译时,统一在html特定位置插入导航html片段 | 无 | 最快 | 很好 | 导航html片段在前端项目,需维护好它 |
通过script动态引入导航js,运行时插入html片段 | 无 | 快 | 一般 | 同上 |
基于框架组件(React、Vue等)做导航栏 | 必须统一框架 | 快 | 一般 | 同上 |
基于微前端做导航栏,导航属于主应用,工具页面属于子应用 | 无 | 一般 | 一般 | 同上 |
我个人是选择了方案二,代码参考: github.com/HullQin/tool-hullqin-cn
效果如下: tool.hullqin.cn
写在最后
我是HullQin,公众号线下聚会游戏的作者(欢迎关注我,交个朋友)。转发本文前需获得作者HullQin授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费无广告。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这个专栏里分享:《教你做小游戏》。