玩群晖NAS的人应该都有过这样的经历DSM装完以后套件中心里躺着一个叫Web Station的套件很多人看了一眼名字就划走了觉得这是搞Web开发才会用到的东西。我自己一度也是这么想的直到有一天受够了浏览器里收藏夹越积越乱、换了设备就找不着链接的麻烦才重新注意到它。结果花了一个晚上用Web Station从零搭起了一个完全属于我自己的个人静态导航站用到现在快两年了稳定得几乎忘了它的存在。这篇内容就围绕这个事情展开把整个搭建过程拆开揉碎讲清楚为什么选Web Station而不是Docker虚拟主机到底怎么配静态导航页的代码结构怎么组织以及内网外网访问、HTTPS配置、日常维护里那些容易踩的坑。不管你是第一次接触Web Station的新手还是已经在用NAS但是没认真折腾过静态网站的玩家看完之后应该都能自己动手搭一个能用的导航站出来。1. 内容整体设计与思路拆解1.1 为什么偏偏用Web Station搭静态导航站先回答一个很实际的问题搭个导航页而已方法多的是为什么要用Web Station我当时对比过几条路线。第一是直接在本地浏览器里用收藏夹这个最省事但换电脑、换浏览器、用手机的时候就全乱了而且收藏夹一多基本等于没有。第二是装第三方导航套件比如Homepage、Dashy这类功能确实强大但很多依赖Docker运行对NAS的性能有一定要求配置起来也更重。第三就是Web Station加纯静态页面这也是我最终选定的方案。Web Station本质上是群晖把Nginx或Apache等Web服务器封装成的一个套件它让用户不用SSH进系统敲命令在图形界面里就能完成网站目录、端口、PHP版本等配置。对于静态导航站这种需求它天然契合我们的页面只是HTML、CSS、JavaScript文件不需要PHP和数据库这意味着响应速度快、占用资源极低哪怕在J系列这种低端NAS上跑也毫无压力。还有一个让我坚定选择Web Station的原因可维护性。导航站的本质是“数据的组织与展示”而纯静态站的数据可以独立拆成一个JSON文件我只需要维护这个文件就行。增删链接不用进后台、不用连数据库在电脑上改完文件传上去刷新页面就生效整个流程非常丝滑。1.2 静态导航站到底需要哪些核心能力很多人以为导航站就是把一堆链接堆在页面上其实真正用起来才知道能让日常使用效率提升的导航站至少要具备以下几点。分类管理。我个人的链接有好几种类型工作用的协同办公地址、开发用的服务器面板、追剧看电影的资源站、买NAS配件逛的论坛混在一起根本没眼看必须按类目隔开。快速检索。这个太重要了。当导航站里存了上百个链接以后靠鼠标翻分类已经很低效。敲键盘就能过滤出想要的站点这个体验和浏览器地址栏直接输域名已经很接近但胜在能帮你想起某个记不清完整域名的网站。高频优先。每天都用的链接比如NAS管理后台、路由器的管理页应该排在显眼的位置不用每次都在分类里翻找。这个可以通过简单的访问计数排序来实现。多端适配。导航站除了在电脑上要用手机浏览器也得能看。布局不能只在宽屏下好看窄屏下也要能浏览。这块对CSS的响应式能力是个小考验。整体设计思路就是“轻量、快、不折腾”。Web Station负责把网页文件发布出去JavaScript负责读取JSON数据并渲染出导航页面数据与展示分离后续的每一次更新成本都被压到最低。2. 核心细节解析与实操要点2.1 环境准备DSM套件安装的微妙之处在DSM里安装Web Station本身很简单打开套件中心搜索就能装。但你如果直接点安装后面可能会遇到问题。我建议装的时候顺手把下面这几个组件也一并勾选安装Web Station、Apache HTTP Server 2.4、Nginx、PHP 8.2或当前主流版本。之所以连PHP也带上虽然静态站用不到但Web Station的“脚本语言设置”里如果没有任何PHP版本有些虚拟主机配置会提示不完整。哪怕页面全是静态的也建议保留一个PHP版本在那里避免后续加个小工具还得回头补装。另外DSM版本不同Web Station的界面布局会有些差异。我一开始用的是DSM 6.2后来升级到DSM 7.2最大的变化是DSM 7以后Web Station的后端服务器默认是Nginx配置入口也重新组织了。升级到DSM 7的时候原有的虚拟主机配置会自动迁移如果你是从旧版本升上来的记得去检查一遍端口和文档根目录有没有变化。提示安装套件的顺序建议是先装Web Station再让它自动关联Apache和Nginx最后再装PHP。手动逐个安装也不是不行但容易出现套件之间关联状态识别不准确的问题。2.2 虚拟主机参数配置端口、根目录、后端服务器安装完成后打开Web Station主界面左侧菜单里找到“网页服务门户”然后点击“新增”——这一部就是创建虚拟主机。这里有几个参数需要重点说。门户类型选择“基于端口”。如果你只有一个站点用基于端口的方式最简单方便直接分配一个独立端口给导航站。基于名称的虚拟主机需要配置域名解析适合有多站点时使用新手阶段没必要为这个增加复杂度。HTTP后端服务器在这里我选的是Nginx。相比ApacheNginx处理静态文件的效率更高、内存占用更小这在高并发下载静态资源时差距很明显。不过对于个人导航站来说两者其实都绰绰有余选Nginx是出于更“合理”的配置习惯。文档根目录是需要额外留意的关键点。Web默认的路径是/web这个目录在DSM的文件管理器里能看到不需要额外权限配置。我建议为导航站单独建一个子目录路径类似/web/nav或者/web/startpage。这样做的目的是把导航站和其他可能发布的Web内容隔离升级文件或备份的时候不会互相影响。这里还有个不起眼但容易出问题的地方Web Station新建虚拟主机时有一个“HTTP后端服务器”的下拉选项很多教程会让你选“Apache”但实测下来“Nginx”在静态页面场景下的响应速度和稳定性都更好。建议直接选Nginx。2.3 文件上传与目录权限管理的坑创建好虚拟主机之后把写好的站点文件传到刚才设置的文档根目录就行。传文件的方式有很多种最简单的是用DSM的File Station网页上传。但如果你要经常更新导航数据文件网页上传效率太低我强烈建议同时打开群晖的WebDAV服务或SMB共享直接把NAS的web/nav目录映射到本地电脑改完文件保存即上传非常方便。说到权限这块有一个我踩过印象很深的坑Web Station在DSM 7里默认跑在一个独立的系统账户下如果你的页面文件是通过SMB拷贝进去的文件属主可能是你的管理员账户这通常没问题。但如果你设置了过严的目录权限Web服务器可能没有读取权限浏览器访问时直接报403 Forbidden。解决办法是检查文档根目录及所有上级目录的读取权限确保Web Station使用的系统用户组能读。在File Station里右键文件夹进入“属性→权限”确认勾选了正确的读取权限就行。这个过程很反直觉因为文件明明存在但Web服务器读不到初学者很容易卡在这里。3. 实操过程与核心环节实现3.1 导航站文件结构与数据组织整个导航站的代码结构非常简洁我在服务器上维护的文件只有三四个/web/nav/ ├── index.html # 页面骨架 ├── style.css # 样式 ├── app.js # 渲染与交互逻辑 └── data/ └── sites.json # 导航数据纯JSON格式有人可能会问为什么不直接用JavaScript把数据写死在app.js里这个问题的答案是“维护成本”。把数据单独放一个JSON文件意味着改导航站内容的时候只需要动一个文件永远不会因为手误动到渲染逻辑。也方便以后写脚本自动化维护这个清单。下面这个sites.json就是一个典型的结构每个分类对应一个数组数组里的每个对象代表一个网站链接{ categories: [ { name: 常用工具, icon: tools, links: [ { title: NAS管理后台, url: http://192.168.1.100:5000, description: 群晖DSM管理页面, frequency: 0 }, { title: 路由器管理, url: http://192.168.1.1, description: 家庭网络设备后台, frequency: 0 } ] }, { name: 开发资源, icon: dev, links: [ { title: GitHub, url: https://github.com, description: 代码托管与开源社区, frequency: 0 } ] } ] }frequency字段就是用来记录访问次数的后面在渲染的时候按它排序用得多的链接自动排到前面。结构上我只分了三级分类、链接、链接属性。再复杂就没有必要了导航站的核心是“快”不是“重”。3.2 页面骨架与样式实现index.html的骨架也保持极简。一个顶部的搜索框一个侧边栏放分类列表主区域放链接卡片。这样的大结构对用户来说几乎没有学习成本。这里有一段核心的页面结构!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title个人导航/title link relstylesheet hrefstyle.css /head body header classnav-header input typesearch idsearchInput placeholder输入关键词快速过滤站点... /header div classnav-container aside classnav-sidebar idcategoryList/aside main classnav-content idlinkGrid/main /div script srcdata/sites.json typeapplication/json idsiteData/script script srcapp.js/script /body /html这里有一个关于加载JSON的小细节值得说明script标签直接加载JSON文件时文件内容会被当作JavaScript解析其中的中文引号或特殊字符可能会引发报错。为了绕开这个问题我在app.js里用的是fetch异步请求而不是传统的外部JSON引用。上面示例里保留这个标签是为了让你看到一种常见写法但在实际部署时我会建议删掉这个script标签改用fetch(data/sites.json)拉取数据。样式方面我的设计原则是“少即是多”。底色用了低饱和度的灰色卡片用白色背景加细阴影在深色环境下看久了也不会觉得刺眼。栅格布局用CSS Grid实现根据屏幕宽度自动调整每行卡片的数量.link-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; padding: 16px; } .card { background: #ffffff; border-radius: 8px; padding: 16px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); transition: transform 0.15s ease, box-shadow 0.15s ease; } .card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.12); }这套样式用下来给人的感觉就是“干净”页面加载时几乎没有视觉噪音信息层级靠间距和阴影区分不靠花哨的配色。3.3 JavaScript渲染逻辑与搜索过滤app.js是整个导航站的大脑负责把JSON数据渲染成页面结构同时处理搜索关键词的实时过滤。核心函数有两个renderAll()负责完整渲染filterSites(keyword)负责搜索结果过滤。async function loadSiteData() { const res await fetch(data/sites.json); if (!res.ok) { console.error(加载站点数据失败, res.status); return; } const data await res.json(); window.siteData data; renderAll(data); } function renderAll(data) { const sidebar document.getElementById(categoryList); const grid document.getElementById(linkGrid); sidebar.innerHTML ; grid.innerHTML ; data.categories.forEach((category, index) { // 侧边栏分类名 const catBtn document.createElement(div); catBtn.className category-item; catBtn.textContent category.name; catBtn.dataset.index index; catBtn.addEventListener(click, () showCategory(index)); sidebar.appendChild(catBtn); // 渲染该分类下的链接卡片 const section document.createElement(section); section.className link-section; section.dataset.categoryIndex index; const h2 document.createElement(h2); h2.textContent category.name; section.appendChild(h2); const cardGrid document.createElement(div); cardGrid.className link-grid; // 按访问次数降序排序访问多的排前面 const sortedLinks [...category.links].sort((a, b) b.frequency - a.frequency); sortedLinks.forEach(link { const card document.createElement(a); card.className card; card.href link.url; card.target _blank; card.rel noopener noreferrer; card.innerHTML div classcard-title${link.title}/div div classcard-desc${link.description}/div ; card.addEventListener(click, () recordClick(link)); cardGrid.appendChild(card); }); section.appendChild(cardGrid); grid.appendChild(section); }); }这里我给每个链接卡片绑定了click事件点击时调用recordClick把frequency加一。由于frequency是存在本地浏览器里的所以这个计数不需要服务端参与实现成本几乎为零。搜索过滤的逻辑也很直白监听输入框的input事件当用户输入关键词时把所有分类里“标题或描述包含关键词”的链接筛出来重新渲染document.getElementById(searchInput).addEventListener(input, function(e) { const keyword e.target.value.trim().toLowerCase(); if (!keyword) { renderAll(window.siteData); return; } const filtered { categories: [] }; window.siteData.categories.forEach(category { const matchedLinks category.links.filter(link link.title.toLowerCase().includes(keyword) || link.description.toLowerCase().includes(keyword) || link.url.toLowerCase().includes(keyword) ); if (matchedLinks.length 0) { filtered.categories.push({ ...category, links: matchedLinks }); } }); renderAll(filtered); });这套逻辑的好处是“每次渲染都是全量渲染”数据量不大时性能完全没问题代码复杂度却因为这个简单粗暴的方案直线下降不需要做增量更新的状态管理。3.4 访问计数与高频置顶的实现细节记录访问频率时我用的是localStorage这比Cookie更可靠容量更大也不会每次请求都往服务器发。function recordClick(link) { const key nav_${link.url}; let count parseInt(localStorage.getItem(key) || 0, 10); localStorage.setItem(key, (count 1).toString()); link.frequency count 1; }实际渲染时每条链接的frequency初始值从JSON里读点击后在本地叠加。这样就算换了一台电脑由于初始值还在JSON里页面依旧会保持合理的排序。本地的点击数据相当于在初始权重之上做了一个个性化调整两不耽误。这个方案在数据量不大的情况下非常稳。我的导航站里现在有大概150个链接每次排序顶多计算几百次浏览器执行起来根本感知不到耗时。注意在用fetch加载sites.json时如果你在本地直接用file://协议打开HTML调试会碰到跨域拦截问题。解决办法是在NAS上用Web Station访问或者本地临时起一个HTTP服务比如Python的python3 -m http.server再调试。4. 内网访问、外网访问与HTTPS配置4.1 局域网设备访问导航站做完了页面第一波验证就是局域网访问。虚拟主机配置好之后在浏览器里输入http://NAS的IP地址:端口号就能看到导航站。比如NAS的局域网IP是192.168.1.100设置的端口是8080那地址就是http://192.168.1.100:8080。这里有一个细节DSM自己的登录端口默认是5000/5001导航站要另选一个不冲突的端口。我当时选的8080顺手在路由器里做了固定IP绑定这样NAS的IP不会因为DHCP租约过期而变化导航页的地址也就能一直稳定使用。如果想把导航站设成路由器DHCP给每台设备发DNS时自动弹出的默认页面可以更进一步把网关地址的某个端口映射到导航站上但这需要路由器支持操作复杂度高一些我目前没做这个个人导航站还没有重要到要抢占首页的程度。4.2 外网访问与群晖DDNS方案人在外面的时候也想访问家里的导航站。群晖原生提供了DDNS服务在控制面板里开启就行它会把一个群晖分配的域名动态解析到你家的公网IP上。只要路由器做了端口转发把外网的一个端口转发到NAS的8080就能通过域名:端口的方式从外网访问导航站。端口转发的配置在路由器后台需要做一步“外部端口到内部端口”的映射。这里有个实际操作经验外部端口尽量不要用80和8080这类常见端口很多运营商对家用宽带的这些端口有限制直接转发出去大概率不通。我当时给外网访问用的是18080这种高位端口从家里出去基本没碰到问题。注意如果你没有公网IP而是属于运营商分配的私网IP环境那么群晖DDNS方案会失效。这种情况下就需要借助内网穿透工具来实现外网访问。内网穿透的选型属于另一个话题导航站本身不需要为此做额外改动只需要穿透工具把流量转发到NAS的8080端口就行。4.3 HTTPS证书申请与强制跳转现在浏览器对HTTP站点越来越不友好尤其是有密码输入、涉及隐私Cookie的页面不带HTTPS的时候浏览器会直接提示“不安全”。导航站虽然没有敏感数据但地址栏上挂个“不安全”总归难看而且有HTTPS之后在外网访问时会少很多兼容性问题。群晖的控制面板里自带“证书”管理功能可以请求Lets Encrypt的免费证书操作非常简单。申请证书时需要域名已经通过DDNS解析到NAS当前公网IP并且80端口或443端口能从外网访问到NAS。群晖在申请证书时会在验证阶段临时起一个HTTP服务来响应验证请求。申请到证书之后再把虚拟主机配置里的“启用HSTS”“启用HTTP/2”打开然后设置一个HTTP到HTTPS的跳转规则。群晖Web Station上实现HTTP到HTTPS跳转有多种方式最简单的做法是在“门户设置”里把HTTP端口设置成跳转到HTTPS端口。有些版本的Web Station界面甚至可以直接在“网页服务门户”里给虚拟主机指定证书。实测下来最稳妥的做法还是申请证书后在虚拟主机的编辑页面里把证书绑定到导航站门户然后再到“门户设置→高级设置”里启用“自动从HTTP重定向到HTTPS”。这套配置完成之后访问http://域名:18080会自动跳到https://域名:18443。全站HTTPS加密浏览器地址栏的小锁也回来了心理上舒服很多。5. 常见问题与排查技巧实录5.1 页面打不开权限、端口与防火墙导航站搭建初期遇到最多的一类问题就是“浏览器访问不了”。第一步排查目标端口。在NAS本机访问http://localhost:8080如果能打开说明Web Station本身没问题问题在网络层。此时检查路由器的端口转发和NAS的防火墙。群晖DSM默认自带防火墙功能如果你之前手动开过防火墙并设置了规则放行了5000端口却没放行8080那么内网的其他设备也访问不了。第二步确认目录权限。如果浏览器访问时出现403 Forbidden大概率是Web服务器没有文档根目录的读取权限。这个问题我在前文提到过排查方法是在File Station里看看文件是不是可读。第三步检查进程状态。Web Station的“网页服务门户”界面里可以看到每个虚拟主机的状态如果显示“异常”或者“已停止”点进去看错误日志多半有详细的报错原因。我最常遇到的错误日志是端口被占用这通常是因为别的套件或Docker容器先占用了同一个端口。5.2 样式加载不出来或者页面纯文本如果页面显示了HTML但没有任何样式打开浏览器开发者工具F12切到Network面板大概率能看到style.css请求的状态码是404或403。404的原因是路径写错了。我建虚拟主机时把文档根目录设成了/web/nav但HTML里链接的是/style.css而不是./style.css那么请求的完整路径就变成了/style.css自然找不到文件。解决办法是尽量在HTML里使用相对路径比如./style.css、./app.js这样页面在哪个子目录下都能正确加载。403的原因则回到了权限问题和前面说的目录不可读是同一个根因。这个坑对新手来说特别容易踩因为本地用编辑器预览HTML时相对路径和绝对路径的效果是相同的一旦到了服务器目录结构下才暴露问题。5.3 内网能访问、外网总超时这类问题排查思路比我刚才说的要复杂一些。先确认外网地址的端口是否通。最简单的方法是手机流量开热点用流量访问http://域名:端口这一步能快速判断当前网络环境是不是有公网IP的“假外网”。如果手机流量访问不了而家里宽带访问正常基本可以确定是端口转发或运营商封锁的问题。下一步检查NAS上是否同时存在多条端口转发规则。很多时候用户调整过路由器配置旧的转发规则没清理新规则又加了上去两条规则冲突导致转发失败。这个要进路由器后台把相关规则全部理一遍。还有一个很容易被忽略的点群晖自带了一个“路由器配置”功能在控制面板→外部访问里如果NAS没有通过UPnP自动注册过端口转发规则而路由器又开启了AP隔离或访客网络隔离那么即使端口规则存在从外网来的流量也可能无法到达NAS。这就需要在路由器上关闭AP隔离或者把NAS单独设成固定IP并加入例外列表。5.4 导航站数据备份与日常维护导航站建好之后真正的长期成本在数据维护。我的更新习惯是每周末花几分钟过一遍data/sites.json把失效的链接删掉把新发现的网站加进去。数据备份方面web/nav这个目录包含全部代码和数据加上DSM自带的Hyper Backup把它们纳入备份任务就行。NAS本身还有快照功能为web目录开启快照误删数据也能随时找回。提示如果某天你发现导航站页面打不开了第一件事不是重装Web Station而是检查NAS存储空间是不是满了。我遇到过两次类似情况都是因为快照占满了存储池导致套件写入异常页面直接白屏。最后再分享一个让维护更省力的小技巧我个人体验下来这套“Web Station 静态JSON”的方案最大的优势不是搭建过程有多快而是日常维护极其轻量。浏览器收藏夹越堆越乱的问题彻底消失了所有链接都以文本格式存在自己手里想迁移就复制走想批量改就写个脚本处理没有任何厂商绑定。如果你把导航站搭起来了建议后面可以试着加上一个新功能直接把sites.json放进一个Git仓库每次修改后提交一次这样每一次增删链接都有历史记录哪次改坏了也能一键回滚。和群晖的Drive套件配合使用还能实现多设备同步维护在电脑上编辑完自动同步到NAS再从手机浏览器打开就是最新版。这个体验用下来非常舒服值得试一试。