1. 为什么我要自己动手改 HomeAssistant 的前端HomeAssistant 这个开源智能家居平台玩过的人都知道它的默认前端界面走的是“功能优先”的路线。仪表盘、卡片、侧边栏能用但要说好看那真是见仁见智。尤其是当你把家里几十个设备接进去之后默认的 Lovelace 界面在信息密度和视觉层次上就开始显得力不从心了。我自己的情况是家里跑了大概四十多个实体灯光、传感器、空调、窗帘、摄像头全接进来了默认面板一打开密密麻麻一片家里人根本不想用。于是我就动了改前端的念头。最开始想的是走主题和自定义卡片这条路HACS 装了一堆前端插件什么自定义卡片、主题包、布局工具折腾了两周效果确实有提升但有些东西改不动。比如登录页的样式、侧边栏的结构、顶栏的按钮布局这些属于前端源码层面的东西靠主题和插件覆盖不了。你要么忍着要么就得动源码。动源码有两条路一条是直接在 HomeAssistant 的 Python 包目录里改前端编译产物另一条是自己构建一个自定义的 Docker 容器在镜像构建阶段把前端源码改好、编译好再打包进去。第一条路的问题是每次 HomeAssistant 升级你的修改全没了而且改的是编译后的压缩文件可读性极差。第二条路虽然前期麻烦一点但一劳永逸升级的时候只需要重新构建镜像修改以补丁的形式保留在 Dockerfile 里干净、可维护。这篇文章就是把我自己构建自定义 HomeAssistant Docker 容器、修改前端源码的完整过程拆开来讲。从环境准备、源码获取、前端修改、编译打包到容器构建、部署验证每一步我都会说清楚为什么这么做、坑在哪里、怎么绕过去。适合有一定 Linux 和 Docker 基础、想把 HomeAssistant 界面真正改成自己想要的样子的人。如果你只是想让界面好看一点HACS 主题可能就够了但如果你要改的是结构层面的东西那这篇文章就是写给你的。2. 整体方案设计与技术选型拆解2.1 为什么选择自定义 Docker 镜像而不是直接改容器很多人第一反应是我直接docker exec进容器找到前端文件改了不就行了短期看确实可以但有几个致命问题。第一HomeAssistant 的前端资源是编译后的 JavaScript 和 CSS经过压缩和混淆你改起来极其痛苦改错一个字符整个界面白屏。第二容器重启后修改就没了因为 Docker 容器的可写层不是持久化的。第三HomeAssistant 升级时新镜像会覆盖所有文件你的修改全部丢失。自定义 Docker 镜像的思路是以官方镜像为基础在构建阶段把前端源码拉下来、改好、编译好然后把编译产物复制到镜像里的正确位置。这样每次构建出来的镜像就是一个完整的、带你自己前端修改的 HomeAssistant。升级的时候你只需要更新基础镜像的版本号重新构建一次修改依然保留。这个方案的核心优势是可重复、可版本控制、可升级。2.2 前端源码的获取方式与版本匹配HomeAssistant 的前端代码是独立仓库叫frontend和核心的core仓库分开维护。这一点很关键因为前端版本和核心版本之间有对应关系。你不能随便拿一个最新版的前端去配一个旧版的核心反之亦然。官方在每次发布时会在核心仓库里指定所使用的前端版本。获取前端源码的正确方式是先确定你要构建的 HomeAssistant 核心版本然后在核心仓库的package.json或者相关配置里找到对应的前端版本号再从这个版本号对应的标签拉取前端源码。我自己的做法是在 Dockerfile 里用ARG传入核心版本和前端版本构建时自动拉取对应标签的源码。这样版本管理清晰不会出现不匹配的问题。2.3 编译工具链的选择与容器内构建策略前端源码是 TypeScript 写的需要 Node.js 环境来编译。编译过程大概需要几分钟取决于机器性能。这里有两种策略一种是在宿主机上编译好把产物复制进镜像另一种是在 Docker 构建阶段用多阶段构建在一个 Node.js 阶段编译然后把产物复制到最终的 HomeAssistant 镜像里。我推荐第二种也就是多阶段构建。原因是宿主机上不一定有正确的 Node.js 版本而且多阶段构建让整个流程完全在 Docker 里完成可重复性更强。你只需要一个 Dockerfile在任何装了 Docker 的机器上都能构建出一模一样的镜像。具体来说第一阶段用node:18-alpine或者类似的基础镜像把前端源码复制进去安装依赖执行编译第二阶段以官方 HomeAssistant 镜像为基础把第一阶段的编译产物复制到前端资源目录。2.4 修改策略补丁文件还是直接改源码修改前端源码有两种方式一种是直接在 Dockerfile 里用sed或者RUN命令改文件另一种是提前准备好补丁文件构建时用patch命令应用。我强烈推荐补丁文件的方式。原因是sed命令对于复杂的修改很难写而且容易出错补丁文件可以本地改好、测试好生成 diff然后在 Dockerfile 里应用清晰、可审查、可回滚。具体操作是先把前端源码克隆到本地改好你要改的文件用git diff生成补丁文件然后把补丁文件和 Dockerfile 放在一起。构建时先拉取原始源码应用补丁再编译。这样你的修改就是一份独立的补丁文件版本控制友好别人也能看懂你改了什么。3. 核心细节解析与实操要点3.1 前端源码中哪些文件值得改HomeAssistant 前端源码结构比较清晰主要目录有src/、src/components/、src/panels/、src/dialogs/、src/resources/等。我自己的修改主要集中在几个地方。登录页在src/panels/config/或者src/auth/相关目录下具体路径随版本变化。侧边栏在src/components/ha-sidebar.ts或者类似文件里。顶栏在src/components/ha-top-app-bar.ts或者src/panels/lovelace/下面。主题变量在src/resources/theme/目录下CSS 变量定义在src/resources/styles.ts或者类似文件里。我建议第一次改的时候先从主题变量入手改几个颜色和圆角编译看看效果确认整个流程跑通了再去改结构性的东西。不要一上来就动侧边栏的布局逻辑那样调试成本很高。3.2 编译命令与产物路径的确认前端源码的编译命令通常在package.json的scripts里定义。常见的是npm run build或者yarn build。编译产物一般在dist/或者build/目录下。你需要确认产物目录里有哪些文件以及这些文件在 HomeAssistant 镜像里的目标路径是什么。官方 HomeAssistant 镜像里前端资源通常在/usr/src/homeassistant/homeassistant/components/frontend/或者类似的路径下。具体路径可以用docker run --rm --entrypoint sh homeassistant/home-assistant:版本号 -c find / -name frontend* -type d来找。找到之后把编译产物复制过去覆盖原有文件。这里有个细节HomeAssistant 的前端资源可能带有版本哈希比如frontend-abc123.js。你编译出来的文件名可能和原来的不一样。这时候要么改编译配置让文件名匹配要么在复制的时候重命名。我自己的做法是保留编译产物的原始文件名然后在复制时把目标目录里的旧文件删掉再把新文件放进去。这样虽然文件名变了但 HomeAssistant 的入口 HTML 会引用新的文件名只要入口文件也一起更新就没问题。3.3 Dockerfile 的结构与关键指令一个典型的多阶段 Dockerfile 大概长这样第一阶段用 Node.js 镜像设置工作目录复制前端源码安装依赖应用补丁执行编译。第二阶段用官方 HomeAssistant 镜像把第一阶段的编译产物复制到前端资源目录覆盖原有文件。关键指令有几个。ARG用来传入版本号方便构建时指定。COPY用来复制源码和补丁。RUN用来执行安装、打补丁、编译命令。FROM用来指定基础镜像。注意第二阶段的基础镜像版本要和你的 HomeAssistant 核心版本一致否则可能出现前端和核心不匹配的问题。还有一个细节官方 HomeAssistant 镜像默认以root用户运行但前端资源目录的权限可能有限制。复制文件的时候要注意权限必要时用chown改一下所有者。我自己的经验是直接COPY过去通常没问题但如果遇到权限报错就在COPY之后加一条RUN chown -R root:root /目标路径。3.4 构建缓存与加速技巧前端编译比较耗时每次构建都重新安装依赖和编译会让人崩溃。Docker 的层缓存机制可以帮上忙。把不常变的部分放在 Dockerfile 前面常变的部分放在后面。比如先复制package.json和锁文件安装依赖这一步的缓存会在依赖不变时复用。然后再复制源码应用补丁编译。这样改源码的时候依赖安装那一步不会重跑。另外如果国内网络环境拉取 npm 包比较慢可以在 Dockerfile 里配置 npm 镜像源。在RUN npm install之前加一条RUN npm config set registry 镜像地址。这个镜像地址用国内常见的公共 npm 镜像就行具体地址我就不写了搜一下就有。编译阶段的 Node.js 基础镜像也可以换成国内可访问的镜像地址加快拉取速度。4. 完整实操流程与核心环节实现4.1 环境准备与目录结构规划先在宿主机上建一个工作目录比如~/ha-custom/。里面放几个东西Dockerfile、patches/目录用来放补丁文件、build.sh构建脚本、docker-compose.yml部署文件。目录结构大概是这样ha-custom/ ├── Dockerfile ├── patches/ │ └── custom-frontend.patch ├── build.sh └── docker-compose.yml宿主机需要装 Docker 和 Docker Compose。版本不要太老建议 Docker 20.10 以上Compose v2 以上。磁盘空间留够前端编译加上镜像层大概需要几个 GB。内存建议 4GB 以上编译过程比较吃内存。4.2 本地修改前端源码并生成补丁先把前端源码克隆到本地临时目录。假设你已经确定了前端版本号比如20240101.0那就git clone --branch 20240101.0 --depth 1 前端仓库地址。然后进入目录安装依赖跑一次编译确认原始源码能编译通过。这一步很重要先确认基线是好的再改。改的时候我建议一次只改一个地方改完编译一次看看效果。比如先改主题颜色在src/resources/theme/下面找到定义主色的地方把默认的蓝色改成你想要的色值。改完执行npm run build看编译是否通过。通过之后用git diff生成补丁git diff ../patches/custom-frontend.patch。补丁文件生成后你可以用git apply --check验证一下补丁是否能干净地应用到原始源码上。命令是git apply --check ../patches/custom-frontend.patch在原始源码目录里执行。如果没有报错说明补丁没问题。4.3 Dockerfile 编写与多阶段构建配置下面是我自己用的 Dockerfile 结构你可以直接参考# 第一阶段编译前端 FROM node:18-alpine AS frontend-builder ARG FRONTEND_VERSION20240101.0 ARG FRONTEND_REPO前端仓库地址 WORKDIR /build RUN apk add --no-cache git python3 make g RUN git clone --branch ${FRONTEND_VERSION} --depth 1 ${FRONTEND_REPO} . RUN npm config set registry 国内npm镜像地址 RUN npm install COPY patches/custom-frontend.patch /build/custom-frontend.patch RUN git apply /build/custom-frontend.patch RUN npm run build # 第二阶段构建最终镜像 FROM homeassistant/home-assistant:2024.1.0 COPY --fromfrontend-builder /build/dist/ /usr/src/homeassistant/homeassistant/components/frontend/ RUN chown -R root:root /usr/src/homeassistant/homeassistant/components/frontend/这里有几个地方需要根据实际情况调整。FRONTEND_VERSION和FRONTEND_REPO要换成你实际使用的版本和仓库地址。npm config set registry那行换成你能访问的 npm 镜像地址。第二阶段的FROM换成你实际使用的 HomeAssistant 版本。COPY的目标路径要确认准确不同版本可能略有差异。4.4 构建镜像与部署验证构建命令很简单docker build -t ha-custom:2024.1.0 .。构建时间取决于网络和机器性能第一次构建大概需要五到十五分钟。构建完成后用docker run或者docker-compose启动。我自己的docker-compose.yml大概是这样version: 3 services: homeassistant: image: ha-custom:2024.1.0 container_name: homeassistant volumes: - ./config:/config ports: - 8123:8123 restart: unless-stopped启动后打开浏览器访问http://宿主机IP:8123看看界面是否正常。如果白屏或者报错先看浏览器控制台有没有 JavaScript 错误再看容器日志docker logs homeassistant有没有报错。常见的问题是前端资源路径不对或者编译产物文件名和入口 HTML 引用的不一致。验证修改是否生效最简单的办法是看你改的那个地方有没有变化。比如你改了主题色登录页或者仪表盘的颜色应该变了。如果没变可能是浏览器缓存强制刷新一下CtrlShiftR。如果还是没变检查一下编译产物是否真的复制到了正确位置可以用docker exec进容器看看文件时间戳。5. 常见问题与排查技巧实录5.1 编译失败与依赖问题排查编译失败最常见的原因是 Node.js 版本不对。前端源码的package.json里通常会指定engines字段要求特定的 Node.js 版本。如果你用的 Node.js 版本太新或太旧编译可能报错。解决办法是看package.json里的要求换成对应的 Node.js 基础镜像。我遇到过用 Node.js 20 编译旧版前端报错的情况换成 Node.js 16 就好了。另一个常见问题是依赖安装失败尤其是网络原因。如果npm install卡住或者报错先检查 npm 镜像源是否配置正确。如果某个包在镜像源里没有可以临时换回官方源试试。还有一种情况是node-gyp相关的编译错误通常是因为缺少 Python 或者 C 编译工具。在 Alpine 镜像里需要apk add python3 make g在 Debian 系镜像里则是apt-get install python3 make g。5.2 补丁应用失败的处理方法补丁应用失败通常是因为源码版本和补丁生成时的版本不一致。git apply会报错告诉你哪个文件哪一行不匹配。解决办法是重新拉取对应版本的源码重新生成补丁。如果你确定版本一致但还是失败可能是补丁文件里的路径和实际路径不一致。用git apply --check -v可以看到详细报错信息根据提示调整。还有一种情况是补丁里有多个文件的修改其中一部分能应用一部分不能。这时候可以用git apply --reject命令它会生成.rej文件告诉你哪些部分失败了。你可以手动把失败的部分改好然后重新生成补丁。我自己的习惯是每次升级前端版本后都重新应用一次补丁确认没问题再构建镜像。5.3 界面白屏与资源加载错误的排查白屏是最让人头疼的问题因为浏览器通常不会告诉你具体哪里错了。排查步骤是这样的先打开浏览器开发者工具看 Console 标签有没有红色报错。常见的报错有Uncaught SyntaxError、Failed to load resource、Unexpected token等。如果是Failed to load resource说明某个 JS 或 CSS 文件没找到可能是路径不对或者文件名不匹配。如果是SyntaxError说明编译产物有问题可能是编译过程中出错了。另一个排查方向是看 Network 标签确认所有资源都返回 200。如果有 404找到那个资源的路径进容器看看文件是否存在。如果文件存在但路径不对检查入口 HTML 里的引用路径。HomeAssistant 的前端入口通常是index.html或者类似文件里面的script标签引用了编译后的 JS 文件。如果文件名变了入口 HTML 也要相应更新。5.4 升级 HomeAssistant 后的适配要点升级 HomeAssistant 核心版本时前端版本通常也会变。你需要重新确认前端版本号重新拉取源码重新应用补丁。如果补丁应用失败说明前端源码有变动你需要手动把修改适配到新版本上。这个过程可能比较繁琐但比每次升级都从头改一遍要好得多。我的做法是每次升级前先把新版本的前端源码拉下来尝试应用旧补丁。如果成功直接构建新镜像。如果失败看看是哪些文件变了手动适配一下重新生成补丁。适配的时候重点关注你修改过的那些文件其他文件的变化不用管。适配完成后本地编译测试确认没问题再构建镜像。5.5 常见问题速查表问题现象可能原因排查方法解决方案编译报错提示 Node.js 版本不符Node.js 版本与前端要求不匹配查看 package.json 的 engines 字段更换对应版本的 Node.js 基础镜像npm install 卡住或失败网络问题或镜像源不可用检查 npm 镜像源配置更换可用的 npm 镜像源补丁应用失败源码版本与补丁不匹配用 git apply --check -v 查看详情重新拉取对应版本源码重新生成补丁界面白屏资源路径错误或编译产物有问题浏览器 Console 和 Network 标签检查文件路径和入口 HTML 引用修改不生效浏览器缓存或文件未正确复制强制刷新进容器检查文件时间戳清除缓存确认复制路径正确容器启动报错权限问题或文件冲突docker logs 查看日志调整文件权限检查文件覆盖逻辑6. 我踩过的坑与实操心得6.1 不要在生产环境直接构建我最开始图省事直接在跑 HomeAssistant 的那台机器上构建镜像。结果构建过程中内存吃满HomeAssistant 容器被 OOM Killer 干掉了家里灯全灭了。后来我学乖了构建镜像在另一台机器上做构建好了推送到私有镜像仓库生产环境只负责拉取和运行。如果你只有一台机器那至少把构建时间安排在半夜并且限制构建过程的资源使用。6.2 保留原始镜像作为回滚方案每次构建新镜像之前我都会把当前运行的镜像打个标签备份一下比如ha-custom:backup-20240101。这样如果新镜像有问题可以快速回滚。回滚命令就是改一下docker-compose.yml里的镜像标签然后docker-compose up -d。这个习惯救过我好几次有一次改侧边栏改出了 bug界面完全没法用回滚之后五分钟就恢复了。6.3 补丁文件要加注释补丁文件本身是可读的但过几个月再看你可能忘了当初为什么这么改。我现在的做法是在补丁文件的开头加一段注释说明这个补丁改了哪些文件、为什么改、对应哪个版本。注释用#开头就行git apply会忽略。这样以后升级适配的时候能快速回忆起修改意图。6.4 前端修改要克制HomeAssistant 的前端代码耦合度不低你改一个地方可能会影响到其他地方的布局。我一开始改得很激进把侧边栏整个重构了结果发现移动端布局全乱了又花了两天时间修。后来我学乖了每次只改一小块改完在桌面端和移动端都测试一遍确认没问题再继续。改前端这件事克制比激进重要。6.5 构建脚本要自动化手动敲docker build命令容易出错尤其是参数多的时候。我写了一个build.sh脚本把版本号、镜像标签、构建参数都封装进去。每次构建只需要./build.sh 2024.1.0就行。脚本里还可以加上构建前的检查比如确认补丁文件存在、确认基础镜像可拉取等。自动化程度越高出错概率越低。这个方案后续还可以这样扩展把构建流程接入 CI/CD每次前端补丁有更新时自动构建镜像并推送到仓库或者把补丁文件拆成多个按功能模块管理比如主题补丁、布局补丁、登录页补丁分开这样升级适配的时候可以逐个应用降低耦合。我自己目前是手动构建因为修改频率不高但如果你改得比较频繁自动化构建值得投入时间去做。