Node.js 真香,但每次部署都想砸电脑

本地运行 npm run dev,一切完美。推到服务器,瞬间崩溃。这就是 Node.js 开发者的日常------开发时真香,部署时真想砸电脑。

周五下午四点,产品说:"这个版本今天必须上线,用户在等。"我看了一眼本地运行得好好的项目,心想:不就是部署吗?半小时搞定。两个小时后,我盯着服务器上那一片红色的报错,手指悬在键盘上,不知道该从哪里开始。外卖凉了,咖啡也凉了,只有我的心,比它们更凉。

那些让人崩溃的部署瞬间

SSH 连上服务器,我深吸一口气,开始了这场"战斗"。首先是拉取代码,这个简单,git pull 一下就好了。然后是安装依赖,我输入了那个熟悉的命令:npm install。看着终端里滚动的日志,我以为一切都会很顺利,但是十分钟后,终端停在了一个错误上:

bash 复制代码
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! node-sass@4.14.1 postinstall: `node scripts/build.js`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the node-sass@4.14.1 postinstall script.

我愣住了。本地明明运行得好好的,为什么到了服务器就不行了?我开始搜索这个错误,Stack Overflow 上有一堆答案,有人说是 Node 版本不对,有人说是 Python 环境问题,还有人说要重新编译 node-sass。我一个个试过去,删除 node_modules,清除缓存,重新安装,换源,升级 Node 版本......折腾了一个小时,终于装上了。但是当我运行 npm run build:h5 的时候,又出现了新的错误:

bash 复制代码
Error: Cannot find module '@dcloudio/uni-cli-shared'
Require stack:
- /root/project/node_modules/@dcloudio/vue-cli-plugin-uni/lib/env.js

我看着这个错误,心里一阵无奈。明明 package.json 里写着这个依赖,为什么找不到?我打开 node_modules 目录,发现这个包确实存在,但是 Node 就是找不到。我开始怀疑是不是依赖树的问题,于是我删除了 package-lock.json,重新安装,还是不行。我又试着用 npm ci 代替 npm install,还是不行。最后我发现,原来是服务器上的 npm 版本太旧了,不支持某些新特性。我升级了 npm,重新安装依赖,这次终于成功了。

但是,当我运行构建命令的时候,又遇到了新的问题。这个项目是一个 uni-app 项目,需要构建成 H5、微信小程序、支付宝小程序等多个平台。我在本地开发时,用的是 Windows 系统,而服务器是 Linux 系统。构建过程中,有些路径分隔符不一样,有些文件权限不一样,导致构建失败。我不得不修改构建脚本,把所有的反斜杠改成斜杠,把所有的绝对路径改成相对路径。改完之后,重新构建,这次终于成功了。看着 dist 目录里生成的文件,我长舒了一口气,以为终于可以上线了。

环境变量的噩梦

构建完成后,我开始配置 Nginx,把静态文件指向 dist 目录。配置完成后,我在浏览器里打开网址,页面倒是显示出来了,但是所有的 API 请求都失败了。我打开浏览器的开发者工具,看到所有的请求都指向了 http://localhost:3000,而不是生产环境的 API 地址。我这才想起来,环境变量没有配置!

在本地开发时,我用的是 .env.development 文件来配置开发环境的 API 地址,而生产环境应该用 .env.production 文件。但是在构建时,我忘记指定环境变量了。我重新运行构建命令,这次加上了环境变量:

bash 复制代码
NODE_ENV=production npm run build:h5

构建完成后,我刷新浏览器,API 请求还是指向了 localhost。我开始怀疑是不是环境变量没有生效,于是我打开构建后的 JS 文件,搜索 API 地址,发现确实还是 localhost。我回到代码里检查,发现原来是 vue.config.js 里的配置有问题,环境变量没有正确注入到代码里。我修改了配置,重新构建,这次终于成功了。

但是,当我测试支付功能的时候,又出现了新的问题。支付需要调用微信支付的 API,而微信支付需要配置商户号、密钥等敏感信息。这些信息不能写在代码里,也不能提交到 Git 仓库,只能通过环境变量来配置。我在服务器上创建了 .env 文件,把所有的敏感信息都写进去,然后重启服务。但是,Node 进程读取不到这些环境变量。我查了一下文档,发现原来 Node 进程需要用 dotenv 包来加载 .env 文件。我在代码里加上了 require('dotenv').config(),重新部署,这次终于成功了。

npm 包版本冲突的折磨

以为一切都搞定了,我开始测试各个功能。首页正常,课程列表正常,订单确认正常,支付......又出问题了。支付页面一直转圈,控制台报错:

bash 复制代码
Uncaught TypeError: Cannot read property 'xxx' of undefined

我打开代码一看,发现是 crypto-js 这个包的问题。这个包用来加密支付参数,在本地运行得好好的,但是在生产环境就报错了。我检查了一下版本,本地是 4.1.1,服务器上也是 4.1.1,版本一样啊,为什么会报错?我开始怀疑是不是依赖的依赖有问题,于是我用 npm ls crypto-js 查看依赖树,发现这个包被多个其他包依赖,而且版本不一样。有的是 4.1.1,有的是 3.3.0,导致冲突了。

我尝试用 npm dedupe 来去重依赖,但是没有效果。我又尝试在 package.json 里加上 resolutions 字段,强制所有的 crypto-js 都用同一个版本,但是 npm 不支持这个字段,只有 yarn 支持。我不得不把整个项目从 npm 迁移到 yarn,重新安装依赖,重新构建,重新部署。这次终于成功了,支付功能正常了。

但是,当我测试国际化功能的时候,又发现了新的问题。这个项目用了 vue-i18n 来实现中英文切换,在本地运行得好好的,但是在生产环境,切换语言后,有些文案没有翻译,还是显示的 key。我检查了一下代码,发现是语言包没有正确加载。原来是构建时,语言包被打包到了不同的 chunk 里,而 Nginx 的缓存策略导致旧的 chunk 没有更新。我修改了 Nginx 配置,禁用了 JS 文件的缓存,重新部署,这次终于成功了。

Docker 部署的坑

折腾了一整天,我决定用 Docker 来部署,这样可以避免环境差异的问题。我写了一个 Dockerfile

dockerfile 复制代码
FROM node:14-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build:h5

EXPOSE 8080
CMD ["npm", "run", "serve"]

看起来很简单对吧?我运行 docker build -t my-app .,构建镜像。构建过程很顺利,但是当我运行 docker run -p 8080:8080 my-app 的时候,容器启动失败了。我查看日志,发现是 node-sass 又出问题了。原来 Docker 镜像用的是 Alpine Linux,而 node-sass 需要编译,Alpine 缺少一些编译工具。我不得不在 Dockerfile 里加上安装编译工具的命令:

dockerfile 复制代码
RUN apk add --no-cache python3 make g++

重新构建镜像,这次成功了。但是当我访问网页的时候,发现静态资源加载不出来。我检查了一下,发现是路径问题。Docker 容器里的路径和宿主机不一样,导致 Nginx 找不到文件。我修改了 Nginx 配置,把路径改成容器里的路径,重新部署,这次终于成功了。

但是,当我重启容器的时候,发现所有的数据都丢失了。原来 Docker 容器是无状态的,重启后所有的数据都会清空。我不得不用 Docker Volume 来持久化数据,修改 docker-compose.yml

yaml 复制代码
version: '3'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ./data:/app/data
    environment:
      - NODE_ENV=production
      - API_URL=https://api.example.com

重新部署,这次终于成功了。但是,当我查看容器日志的时候,发现日志文件越来越大,占用了大量的磁盘空间。我不得不配置日志轮转,限制日志文件的大小。折腾了一整天,终于把 Docker 部署搞定了。

pm2 进程管理的痛苦

Docker 虽然解决了环境差异的问题,但是对于一些简单的项目,用 Docker 有点大材小用。我决定用 pm2 来管理 Node 进程,这样可以实现自动重启、负载均衡、日志管理等功能。我在服务器上安装了 pm2:

bash 复制代码
npm install -g pm2

然后创建了一个 ecosystem.config.js 配置文件:

javascript 复制代码
module.exports = {
  apps: [{
    name: 'my-app',
    script: 'npm',
    args: 'run serve',
    instances: 2,
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 8080
    }
  }]
}

运行 pm2 start ecosystem.config.js,进程启动成功了。我用 pm2 list 查看进程状态,一切正常。但是,当我访问网页的时候,发现有时候能访问,有时候不能访问。我查看日志,发现有一个进程一直在重启,报错信息是端口被占用。原来是我配置了 2 个实例,但是它们都监听同一个端口,导致冲突了。我修改了配置,让 pm2 自动分配端口,重新启动,这次终于成功了。

但是,当我更新代码的时候,发现 pm2 没有自动重启。我查了一下文档,发现需要用 pm2 reload 命令来实现零停机更新。我运行 pm2 reload my-app,进程重启了,但是有几秒钟的时间,网站无法访问。我又查了一下文档,发现需要配置 wait_readylisten_timeout 参数,让 pm2 等待应用完全启动后再切换流量。我修改了配置,重新部署,这次终于实现了零停机更新。

那些实用的部署技巧

经过无数次的踩坑,我总结了一些实用的部署技巧,希望能帮到同样在部署中挣扎的你:

依赖管理方面 ,首先要锁定依赖版本,使用 package-lock.jsonyarn.lock 来确保开发环境和生产环境的依赖版本一致。其次,在 package.json 里使用精确版本号,而不是 ^~,避免自动升级导致的问题。如果遇到依赖冲突,可以使用 npm ls 查看依赖树,找出冲突的包,然后用 resolutions 字段(yarn)或 overrides 字段(npm 8+)来强制指定版本。对于 node-sass 这种需要编译的包,建议换成 sass(dart-sass),它是纯 JavaScript 实现的,不需要编译,兼容性更好。

环境变量管理方面 ,不要把敏感信息写在代码里,也不要提交到 Git 仓库。使用 .env 文件来管理环境变量,并在 .gitignore 里忽略这个文件。在代码里使用 process.env.XXX 来读取环境变量,并在构建时通过 webpack.DefinePluginvue.config.jsdefine 选项来注入。对于不同的环境(开发、测试、生产),使用不同的 .env 文件(.env.development.env.test.env.production),并在构建时通过 NODE_ENV 来区分。

构建优化方面 ,使用 npm ci 代替 npm install,它会根据 package-lock.json 来安装依赖,速度更快,也更可靠。在 CI/CD 流程中,缓存 node_modules 目录,避免每次都重新安装依赖。使用多阶段构建(Docker)来减小镜像体积,只把必要的文件打包到镜像里。启用 Gzip 压缩和 Brotli 压缩,减小静态资源的体积。配置 CDN 来加速静态资源的加载。

日志管理方面 ,使用 pm2systemd 来管理进程,它们都支持日志轮转和日志查看。配置日志级别,在生产环境只输出 errorwarn 级别的日志,避免日志文件过大。使用日志聚合工具(如 ELK、Loki)来集中管理日志,方便查询和分析。在代码里加上请求 ID,方便追踪一个请求的完整生命周期。

监控和告警方面 ,使用 pm2 plusNew Relic 等工具来监控应用的性能和错误。配置健康检查接口,让负载均衡器定期检查应用是否正常。配置告警规则,当应用出现异常时,及时通知开发人员。使用 sentry 等工具来收集前端错误,方便排查问题。

常用的部署命令,我整理了一份清单:

bash 复制代码
# 拉取代码
git pull origin main

# 安装依赖(推荐使用 ci)
npm ci

# 构建项目
npm run build:h5

# 使用 pm2 启动
pm2 start ecosystem.config.js

# 查看进程状态
pm2 list

# 查看日志
pm2 logs my-app

# 重启进程(零停机)
pm2 reload my-app

# 停止进程
pm2 stop my-app

# 删除进程
pm2 delete my-app

# 保存 pm2 配置(开机自启)
pm2 save
pm2 startup

# Docker 相关
docker build -t my-app .
docker run -d -p 8080:8080 --name my-app my-app
docker logs -f my-app
docker restart my-app
docker stop my-app
docker rm my-app

# 使用 docker-compose
docker-compose up -d
docker-compose logs -f
docker-compose restart
docker-compose down

开发环境 vs 生产环境的巨大鸿沟

每次部署,我都会感叹:为什么开发环境和生产环境的差异这么大?在本地,我用的是 Windows 系统,Node 14,npm 6,一切都运行得很好。但是到了服务器,Linux 系统,Node 16,npm 8,各种问题就出现了。路径分隔符不一样,文件权限不一样,环境变量不一样,依赖版本不一样,甚至连时区都不一样。

有一次,我遇到了一个非常诡异的问题:在本地,日期格式化的结果是 2024-01-01,但是在服务器上,结果是 2023-12-31。我排查了很久,才发现是时区的问题。本地是东八区,服务器是零时区,导致日期计算出现了偏差。我不得不在代码里显式指定时区,才解决了这个问题。

还有一次,我遇到了一个更诡异的问题:在本地,图片上传功能正常,但是在服务器上,上传的图片无法访问。我检查了文件路径,检查了文件权限,都没有问题。最后我发现,原来是 Nginx 配置的问题。Nginx 默认不允许访问隐藏文件(以 . 开头的文件),而我上传的图片文件名恰好以 . 开头,导致无法访问。我修改了 Nginx 配置,才解决了这个问题。

这些问题,在开发环境是不会出现的,只有在生产环境才会暴露出来。每次部署,都像是在拆盲盒,你永远不知道会遇到什么问题。有时候是依赖问题,有时候是环境变量问题,有时候是权限问题,有时候是配置问题。每一个问题,都需要花费大量的时间去排查和解决。

写在最后

Node.js 真的很香,开发效率高,生态丰富,前后端统一语言,让全栈开发变得更加容易。但是,部署真的很痛苦。每次部署,都像是在经历一场战斗,你需要和各种环境问题、依赖问题、配置问题作斗争。看着服务器报错,我的心也跟着崩了。有时候我在想,为什么部署不能像开发一样简单?为什么不能一键部署,自动处理所有的问题?

但是我知道,这是不可能的。软件开发本身就是一个复杂的过程,部署只是其中的一个环节。我们能做的,就是尽可能地规范化、自动化,减少人为错误,提高部署效率。使用 Docker 来统一环境,使用 CI/CD 来自动化部署,使用监控工具来及时发现问题,使用日志工具来快速定位问题。

虽然部署很痛苦,但是当你看到网站成功上线,用户能够正常使用,所有的付出都是值得的。这就是我们的工作,也是我们的价值。所以,加油吧,Node.js 开发者。虽然每次部署都想砸电脑,但是我们还是会继续部署,继续优化,继续成长。因为,这就是我们的热爱。


服务器终于部署成功了,但是我的周末又没了。明天还要继续优化性能,修复 Bug,处理用户反馈。Node.js 真香,但部署真的想砸电脑。晚安,服务器。晚安,Bug。晚安,我的周末。

相关推荐
子兮曰10 小时前
OpenClaw入门:从零开始搭建你的私有化AI助手
前端·架构·github
Victor35610 小时前
https://editor.csdn.net/md/?articleId=139321571&spm=1011.2415.3001.9698
后端
吴仰晖10 小时前
使用github copliot chat的源码学习之Chromium Compositor
前端
1024小神10 小时前
github发布pages的几种状态记录
前端
Victor35610 小时前
Hibernate(89)如何在压力测试中使用Hibernate?
后端
灰子学技术12 小时前
go response.Body.close()导致连接异常处理
开发语言·后端·golang
不像程序员的程序媛12 小时前
Nginx日志切分
服务器·前端·nginx
Daniel李华12 小时前
echarts使用案例
android·javascript·echarts
北原_春希12 小时前
如何在Vue3项目中引入并使用Echarts图表
前端·javascript·echarts
JY-HPS12 小时前
echarts天气折线图
javascript·vue.js·echarts