Immich Web 前端项目深度解析:基于 SvelteKit 的构建、开发代理与 SPA 部署机制
Immich 的 web/ 目录承载了整个系统的 Web 界面,它基于 SvelteKit 框架开发:本地开发时以 SvelteKit 的 Vite Node.js 开发服务器形态运行,生产部署时则被构建为 SPA(单页应用)产物,随 server 镜像一起分发并由后端托管静态资源。本文以 web/README.md 为主线,结合 svelte.config.js、vite.config.ts、web/package.json 与 server/Dockerfile,完整讲清该前端项目的技术栈、开发链路(dev server + 后端代理)、版本注入、生产构建流程,以及构建产物如何被 server 容器消费,帮助开发者在动手贡献代码前建立对这套前端工程的完整认知。
项目定位与技术栈选型
web/README.md 开篇即明确了三件核心事实:
- 项目使用 SvelteKit Web 框架(README 原文如此,读者可参见 SvelteKit 官方文档入门);
- 理解 SvelteKit 的文件路由(file-based routing) 是读懂本仓库 Web 代码的前提——
src/routes/下的目录结构直接对应 URL 路径; - 开发态与生产态形态不同:本地开发运行的是 SvelteKit Node.js 开发服务器,而生产环境是构建为 SPA 后并入 server 项目一起部署。
从 web/package.json 可以看到实际锁定的技术栈版本(以当前仓库为准):
| 依赖 | 版本 | 作用 |
|---|---|---|
@sveltejs/kit |
^2.56.1 |
SvelteKit 框架本体 |
svelte |
5.56.9 |
Svelte 5(Runes 时代的响应式语法) |
@sveltejs/vite-plugin-svelte |
7.3.0 |
SvelteKit 依赖的 Vite 插件 |
vite |
^8.0.0 |
开发服务器与构建工具 |
@sveltejs/adapter-static |
^3.0.8 |
静态站点适配器,生产 SPA 的关键 |
tailwindcss / @tailwindcss/vite |
^4.2.4 |
Tailwind CSS v4(Vite 插件方式接入) |
vitest / happy-dom / @testing-library/svelte |
— | 单元测试三件套 |
@immich/sdk |
workspace:* |
与 server 共享的类型化 API 客户端,pnpm workspace 内部依赖 |
maplibre-gl / pmtiles / svelte-maplibre |
— | 地图与瓦片支持 |
hls.js / media-chrome |
— | 视频播放 |
svelte-i18n |
^4.0.1 |
配合仓库根目录 i18n/ 下的 100+ 语言文件做多语言 |
其中两个值得注意的细节:
@immich/sdk: workspace:*:Web 前端并非手写 REST 调用,而是消费仓库内 packages/sdk 的本地包,接口类型由 OpenAPI 规格生成(参见 open-api/immich-openapi-specs.json),保证前后端类型一致;i18n别名:svelte.config.js 中'$i18n': '../i18n'将翻译文件目录直接映射为$i18n导入,而 server/Dockerfile 构建镜像时也显式COPY ./i18n ./i18n/,说明国际化资源是 Web 构建的必需输入。
目录结构:以 SvelteKit 文件路由为骨架
理解 SvelteKit 文件路由后,web/ 的代码组织就很直观了。顶层目录:
web/
├── bin/immich-web # 开发容器入口脚本(等待后端就绪后启动 dev server)
├── src/
│ ├── lib/ # 共享库代码(别名 $lib,约 470 个文件)
│ ├── routes/ # SvelteKit 文件路由:目录即 URL(160+ 个 .svelte 页面)
│ ├── params/ # URL 参数校验器
│ ├── service-worker/ # 离线/Service Worker 相关
│ ├── test-data/ # 测试夹具(别名 @test-data)
│ ├── app.css / app.html / app.d.ts
│ └── hooks.client.ts / hooks.server.ts # 客户端/服务端生命周期钩子
├── static/ # 原样拷贝的静态资源(favicon、PWA manifest 等)
├── tests/ # 共享测试辅助(别名 $tests)
├── eslint.config.js
├── svelte.config.js
├── tsconfig.json
└── vite.config.ts
几个关键别名在 svelte.config.js 中定义:
alias: {
$lib: 'src/lib',
'$lib/*': 'src/lib/*',
$tests: 'src/../tests',
'@test-data': 'src/test-data',
$i18n: '../i18n',
}
$lib 是 SvelteKit 约定俗成的库代码别名;而 $i18n 指到仓库根目录的 i18n/ 文件夹——这也是为什么构建 web 时 i18n 目录必须与 web 目录同处一个构建上下文中。
本地开发:dev server、后端代理与 mise 任务
开发服务器与三个代理前缀
生产环境下 Web 界面与 server 同域同端口(2283)部署,因此前端所有请求都以 /api、/.well-known/immich 等相对路径发出。本地开发时前后端分离(Vite 在 3000 端口、server 在 2283 端口),vite.config.ts 通过 Vite 的 dev proxy 把这三种前缀转发到后端:
const upstream = {
target: process.env.IMMICH_SERVER_URL || 'http://immich-server:2283/',
secure: true,
changeOrigin: true,
logLevel: 'info',
ws: true, // 支持 WebSocket,供 socket.io 实时通知使用
};
const proxy = {
'/api': upstream, // 全部 REST / socket.io API
'/.well-known/immich': upstream, // 实例发现端点(App 配对用)
'/custom.css': upstream, // 自定义 CSS 注入
};
要点:
IMMICH_SERVER_URL环境变量决定上游地址。默认值http://immich-server:2283/面向 Docker 内部网络;本地裸跑时通常指向http://localhost:2283;ws: true使 WebSocket 升级请求也走代理,这是 Web 端实时推送(socket.io-client 在 web/package.json 中依赖)能工作的原因;- 该 proxy 配置同时作用于
server与preview两个环节(vite.config.ts),即vite preview静态预览产物时也保持同样的转发行为。
vite 配置里还有两处开发体验相关设置:server.allowedHosts: true(允许通过非 localhost 域名访问,适配远程开发场景)和 optimizeDeps.entries: ['src/**/*.{svelte,ts,html}'](显式声明预构建扫描入口)。
开发脚本
web/package.json 中的脚本一览:
"dev": "vite dev --host 0.0.0.0 --port 3000", // 本地开发服务器
"build": "vite build", // 生产构建(SPA 静态产物)
"build:stats": "BUILD_STATS=true vite build", // 额外产出 bundle 体积分析 stats.html
"preview": "vite preview", // 本地预览构建产物(带 proxy)
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings ...",
"check:typescript": "tsc --noEmit",
"lint": "eslint . --max-warnings 0 --concurrency 6",
"test": "vitest",
"prepare": "svelte-kit sync" // 安装后自动生成 .svelte-kit 类型/路由信息
build:stats 背后的机制在 vite.config.ts:当环境变量 BUILD_STATS=true 时动态挂载 rollup-plugin-visualizer,构建后输出可视化产物 stats.html,用于分析打包体积构成。
用 mise 一条命令拉起开发环境
仓库统一用 mise 管理工具链与任务。web/mise.toml 定义的任务链很有代表性:
[tasks.start]
run = [
{ task = ":install" }, # pnpm install --filter immich-web --frozen-lockfile
{ task = "//:sdk:install" }, # 安装并 …
{ task = "//:sdk:build" }, # 先构建 workspace 内的 @immich/sdk
"pnpm run dev",
]
也就是说,web 单独开发的前置条件是先构建 @immich/sdk(因为它是 workspace:* 依赖),这正是 server/Dockerfile 中 sdk 构建阶段先于 web 阶段的镜像原因——Web 构建在 FROM sdk AS web 阶段之上复用 SDK 产物。
另一个实用任务是 start-demo:注入 IMMICH_SERVER_URL = "https://demo.immich.app" 后执行 start,即可跳过本地后端、直接代理到官方演示实例做纯前端开发。
Docker 开发模式:bin/immich-web 等待后端就绪
除了裸跑,仓库还提供容器化开发。docker/docker-compose.dev.yml 中定义了 immich-web 服务(镜像 immich-web-dev:latest,命令 immich-web),其入口脚本即 web/bin/immich-web:
cd /usr/src/app || exit
pnpm --filter @immich/sdk build
UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283/}"
# 轮询 GET ${UPSTREAM}/api/server/config 直到后端就绪
until wget --spider --quiet "${UPSTREAM}/api/server/config" > /dev/null 2>&1; do
...
sleep 1
done
pnpm --filter immich-web exec vite dev --host 0.0.0.0 --port 3000
这个脚本值得注意:开发容器里跑的同样是 vite dev(热更新),而非构建产物;它先构建 SDK,再通过 /api/server/config 端点探活后端(每 10 次轮询打印一次等待日志),确保 API 代理不会把请求打到尚未就绪的 server。这也呼应了 vite.config.ts 中那句注释:"connect to a remote backend during web-only development"——该模式既支持代理本地后端,也支持通过 IMMICH_SERVER_URL 指向远端后端。
构建产物如何成为 SPA:adapter-static 与回退路由
web/README.md 最后一句"it is built as a SPA",其技术实现在 svelte.config.js:
kit: {
version: {
name: process.env.IMMICH_BUILD || process.env.npm_package_version || 'local',
},
paths: { relative: false },
adapter: adapter({
fallback: 'index.html',
precompress: true,
}),
...
}
逐项解读:
@sveltejs/adapter-static+fallback: 'index.html':SvelteKit 构建出纯静态文件(无 Node 运行时),所有未被具体页面文件匹配的 URL 一律回退到index.html,由前端路由接管——这就是 SPA 的标准落地方式。没有这一项,深链(deep link)直接访问会 404;precompress: true:构建时预生成 gzip/brotli 压缩版本,静态托管时可直接下发压缩内容;paths.relative: false:资源 URL 使用绝对路径(/_app/...),与 SPA 从任意路径被静态服务器兜底服务的要求一致;version.name:构建版本号取自IMMICH_BUILD(CI 构建 ID)→ 回退到npm_package_version(即 package.json 的3.2.0-rc.0)→ 本地开发则为local。该版本信息会被 SvelteKit 写入应用运行时,供前端展示"当前运行的 Immich 版本",与 server 侧健康检查/状态页对版本的要求保持一致。server/Dockerfile 中ARG BUILD_ID / ENV IMMICH_BUILD=${BUILD_ID}正是这条链路的上游。
版本构建产物的目录约定:build/ → /build/www
vite build 的默认输出目录是 web/build。这一点在 server/Dockerfile 中得到了消费侧印证:
COPY --from=web /usr/src/app/web/build /build/www
而 server 端在 server/src/repositories/config.repository.ts 里定义了静态目录约定:
const buildFolder = dto.IMMICH_BUILD_DATA || '/build';
const folders = {
geodata: join(buildFolder, 'geodata'),
web: join(buildFolder, 'www'),
};
并在 config.repository.ts 将其注册为静态资源配置(root: folders.web、indexHtml: join(folders.web, 'index.html'))。由此形成完整的部署闭环:Dockerfile 把 web/build 拷到 /build/www,server 启动时从 IMMICH_BUILD_DATA(默认 /build)下的 www 子目录提供 Web 界面,与 docker/docker-compose.prod.yml 中 server 容器 2283 端口对外同时暴露 API 与页面的行为一致。
测试与质量保障
Web 项目的质量门禁同样由 web/package.json 与 web/mise.toml 组织:
- 单元测试:Vitest + happy-dom 环境,配置内联在 vite.config.ts(
include: src/**/*.{test,spec}.{js,ts}、TZ: 'UTC'固定时区避免日期断言漂移、setup 文件src/test-data/setup.ts); - 类型与编译检查:
tsc --noEmit与svelte-check --fail-on-warnings双检查(check:code还会串联 prettier 与 eslint); - mise 任务
ci-unit:SDK 安装构建 → install → format → check →test --run的完整链路,checklist任务在其上追加 lint,与 docs/developer/pr-checklist.md 描述的提交前检查项对应。
针对 Svelte 5 的迁移期,svelte.config.js 中留有 // TODO pending '@immich/ui' to enable it / runes: true 注释,onwarn 钩子与 check:svelte 脚本则统一忽略 state_referenced_locally 告警——说明当前代码库正处于 Runes 模式全面开启前的过渡状态,阅读源码时同一组件中混用 $state 与传统 store 是预期现象。
小结
回到 web/README.md 的三条主线,本文已逐一落到可验证的仓库证据上:SvelteKit 文件路由决定了 src/routes/ 的代码组织方式;vite dev --host 0.0.0.0 --port 3000 配合 IMMICH_SERVER_URL 代理(/api、/.well-known/immich、/custom.css,含 WebSocket)构成开发态,mise 的 :start 任务负责 SDK 前置构建与拉起;生产态则通过 adapter-static 的 fallback: 'index.html' 构建出 SPA,IMMICH_BUILD 注入版本号,vite build 产物经 server/Dockerfile 落入 /build/www,由 server 依 config.repository.ts 的约定静态托管。掌握这条从源码到容器的完整链路,即可独立启动 Immich Web 前端开发环境并理解其构建与部署行为。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00