服务端渲染(ssr)项目及nuxt项目部署过程
导言
去年有写一篇关于构建nuxt项目的博客,其中有提到ssr项目部署问题,关于这个实在是可讲的太多,因此单独写了一篇,就是本文。
csr与ssr部署
传统的客户端渲染(csr)项目的部署,即是把webpack打包后生成的静态文件(dist)上传到服务器上,通过配置网关及nginx转发,使外网客户端可以访问到这些html文件。
而服务端渲染(ssr)项目,依赖web服务器动态构建html文件,因此之前的csr方法肯定不得行。从这个思路出发,ssr项目需要在服务器在将web服务器(web server)跑起来,并且使外网客户端可以访问到web服务器(这里与csr部署思路一致,方法略有不同),再根据不同的条件生成不同的首屏html并返回给客户端。
本文主要讲两种部署方法:基础ssr的部署方法(按照vue官网的ssr指南构建的项目)和使用nuxt.js框架的部署方法。
ssr部署方法
以下使用到的命令建议梳理记录,后续可以将这些命令列成一个启动脚本,实现自动化启动。
【p1】web server的部署
想要将web server部署在服务器上,服务器上必须要有node.js环境,才能在web server跑起来之后提供一个node上下文环境,只是硬性要求。测试方法很简单,node命令有效即可,如下:
node -v
如果没有,就需要在服务器上安装一下node.js了。
然后需要将对应的server启动文件(server.js)上传到服务器上,这里大家应该都有自家的文件上传服务器的脚本,不加赘述。
在服务器上试着跑一下web服务器
node ./server.js
发现报错,会提示module not found,原因是server.js文件里可能依赖(require)了其他依赖项,它在本地找不到这些依赖项,一些可能是node.js自身的一些模块,比如fs,path这种,这些依赖并不需要额外引入,因为node环境中已经包含,另一些就是外部的js库,比如koa.js这类web server框架,需要额外引入,该怎么办呢?
// server.js头部引用 let fs = require('fs'); // node模块 const path = require('path'); // node模块 const Koa = require('koa'); // 外部依赖 const Router = require('koa-router'); //外部依赖
答案就是需要把依赖项也同时上传到服务器上,这里有两种方法:一种是直接将依赖下载到项目内,并且通过脚本与server.js文件一同上传至服务器上;另一种是将这些需要额外引入的依赖清单列成一个package.json,跑server.js之前先通过npm install把这些依赖下载下来。
这些依赖需要存放到node_modules文件夹中,并且按照文件夹层级逐级索引,当本文件夹内不存在时则向上一级文件夹索引,而且由于我们的服务器依赖基本上是很少改变的,可以做到不用每次都下载这些体积不算小的依赖,具体的处理方法因人而异,这里只是提出一点思路,希望这点小细节可以帮助你方便处理服务器的依赖项。
当依赖下载完成,再次尝试跑起来web服务器,检查web服务器运行的端口,如果可以响应,这就表示我们完成了第一步。
# 就绪检查命令可以这么写 curl http://host:port/你的检查服务器的路由 # 例如我的server.js是这么写的: // 就绪检查 router.get('/heart-beat', ctx => { ctx.response.code = 200; ctx.response.body = 'ok'; }); const host = process.env.HOST || '0.0.0.0'; const port = process.env.PORT || 8090; app.listen(port, host); console.log(`Server listening on http://${host}:${port}, now is ${new Date().toLocaleString()}`); # 那么检查就绪的命令就可以这么写 curl http://127.0.0.1:8090/heart-beat # web server响应: 'ok' # 这就表示完成了
【p2】静态资源文件的上传
静态资源文件即是指webpack打包后生成的文件,无论是server bundle还是client bundle都应该和server.js一起上传到服务器上,在通过web服务器按条件进行处理,返回给客户端。
在生成server bundle和client bundle方面,这里详述起来会比较复杂,以后会再起一篇文章,讲一下按照vue官方的ssr指南如何构建一个ssr项目。
server bundle交付给web服务器,服务器在处理拼接html时,可能也会用到一些外部依赖,比如vue、axios这类。这些很容易理解:因为它实际上是将你的bundle文件执行了一遍,执行过程中遇到引用的其他依赖,当然需要在当前的环境也有这些依赖。
这里总结一下ssr项目需要在服务器上用到的依赖:
1、server.js require的依赖(node模块除外)
2、src内源码文件在头部import的依赖
3、src内院吗文件在头部import的项目文件内用到的依赖(比如你引用了自己的util/tool.js,在tool.js内import的依赖也需要安装)
当然这些依赖都不会超出你的package.json内依赖的范围,如果你觉得麻烦,也可以直接将package.json内的依赖全部安装到服务器上。
静态资源文件上传完毕以后,就可以按照上面检查web server就绪的方法,检查是否可以完成html文件的组装
curl http://host:port/你的ssr路由
如果web服务器响应并且返回一个html文件,那么恭喜你已经完成了80%的工作。
【p3】node进程的维护管理
完成以上两步,基本上表示你的项目可以正常运行,还差最后一步:如何维护你的node进程正常运行,假设遇到了意外问题,服务崩溃了,如何维护web服务器重启?
pm2应运而生。
在你的服务器上安装pm2,参考pm2官方文档
pm2是一个用于维护node进程的管理工具,通过配置一个简单的文件,即可帮你维护你的node进程,并且可以做到地址端口复用,多实例分流,异常重启等,十分强大。
配置文件如下:
// ecosystem.config.js
module.exports = { apps: [{ name: 'my-ssr-app', script: './build/server.js', // 你的web server入口文件 args: 'start', // 应用启动的路径 cwd: './', // 应用启动模式,支持fork和cluster,cluster支持地址端口复用 exec_mode: 'fork', // 应用启动实例个数 // fork模式下不能起多个实例 会报错 instances: 1, // instances: "max", // 最大内存限制数,超出自动重启 max_memory_restart: '1G', // 监听重启,文件夹变化自动重启 watch: ['dist', 'build'], // 应用运行少于该时间认为启动异常 min_uptime: '3s', // 发生异常的情况下自动重启 autorestart: true, // 最大异常重启次数,即小于min_uptime运行时间重启次数 max_restarts: 3, // 异常重启情况下,延时重启时间 restart_delay: 3000, error_file: './pm2-log/err.log', out_file: './pm2-log/out.log', combine_logs: true, env_production: { 'NODE_ENV': 'production' }, env_release: { 'NODE_ENV': 'release' } }] };
配置完成后,需要通过pm2启动你的web服务器:
// package.json的启动命令 "scripts": { "pm2:release": "pm2 start ecosystem.config.js --name my-ssr-app --env release" }
在通过pm2命令检查你的服务器是否已经启动完成
pm2 ls
常用的pm2命令,如重启实例、删除实例等你可以在网上查询资料。
一些常见问题:
1、cluster模式EBIG问题
关于使用pm2的cluster模式会创建多个实例,自动实现负载均衡,目前仅支持node,相较于fork模式的单实例稳定。
实现cluster模式会复制当前node的环境变量,如果环境变量多于一定值,会报EBIG错误,目前可借鉴的解决方案如下:
(1)每次pm2运行之前清除当前环境的环境变量(可能影响服务器内其他服务,毕竟服务器上一般都不止一个服务)
(2)修改pm2代码,把无用环境变量过滤掉。此方案需要维护一个私有的pm2包
(3)不使用cluster模式,使用fork模式(我现在选用的方案)
参考资料:
https://github.com/Unitech/pm2/issues/3271#issuecomment-512224470
2、部分permission denied问题
请注意你登录服务器的用户身份,最好是管理员,如果不是,可能会导致一些功能异常。
3、日志问题
另外由于pm2会将项目内在node运行期间的日志(console.log)记录下来,生成日志,日志文件保存在配置文件内指定的地方,如果长期没有清理这些日志,则会累积导致内存或者磁盘占用。当然现在的服务器环境在每次启动时都会删除之前的文件夹生成新的文件夹,日志文件也会随之清理。但是如果长期没有重启也会有这个问题,所以建议使用pm2的一个日志管理模块pm2-logrotate,你可以在网上查询到如何配置、使用它。
4、环境变量
比如说运行的环境变量(NODE_ENV),这是一个很重要的变量,用于区分当前运行环境是开发、测试还是线上环境,这个变量不再由node直接注入,而是pm2,这就牵扯到配置文件内,配置环境的问题了:
env_release: { 'NODE_ENV': 'release' }
像这样在配置文件内规定了env_release环境内注入的NODE_ENV,再在启动命令加上--env release,pm2就知道我们需要注入什么环境变量了。
【p4】nginx配置
以上均配置完成后,现在要做的就是要配置nginx让外网客户端可以访问到我们的web服务器。
网关分流完成后,所有对应的流量都会导向到我们服务器上。
配置nginx,将符合条件的访问都导向到我们的web服务器上:
location / { proxy_pass http://127.0.0.1:web服务器运行的端口/; }
修改server.js,保证path一致:
// 最后访问地址:https://你的外网host/网关标识/实际访问路由 router.get(('/网关标识/') + 实际访问路由, ctx => { // do sth... })
在浏览器上访问你的期望的url,现在就可以正常访问到你的ssr页面了。
【p5】总结
1、上传到服务器上的文件清单
dist :打包后的bundle文件
server.js :web服务器的启动文件
package.json :需要用到里面的依赖项列表&部分启动命令也可以存在这个里面
ecosystem.config.js :pm2的配置文件
node_modules :可以手动上传到服务器上,也可以在通过package.json安装之后生成
一些其他脚本文件及server.js里引用的js文件
2、启动命令
所有需要用的命令可以整理在一个sh文件(start.sh)里,然后在Dockerfile里,完成所有配置后,启动它。至此就实现了自动启动了。
# Dockerfile
RUN chmod +x /opt/apps/my-ssr-app/start.sh
# 安装pm2日志管理模块 RUN pm2 install pm2-logrotate CMD /opt/apps/my-ssr-app/start.sh
// start.sh
cd /opt/apps/my-ssr-app npm run pm2:release
nuxt部署方法
nuxt基本帮我们把该做的都做了,服务器也准备好了,都打包在.nuxt里
1、准备必须依赖项
server/index.js下引用的
nuxt.config.js里引用的
一般为modules或者build里提到的模块
2、修改nuxt.config.js
(1)buildDir:生成的构建文件夹名字,不建议使用默认名字,默认名.nuxt是个隐藏文件夹,这会导致在镜像中复制文件时丢文件夹。命名建议不以符号开头,不与路由重复。
(2)env:环境变量,nuxt中环境变量(process.env.NODE_ENV)只有两个固定值(development,production)且不能新增值,如需要自定义别的值,需要在这里声明。
(3)build.publicPath:对资源文件的索引路径,需要与服务器或者CDN上资源文件路径一致,建议使用绝对路径,避免出现问题
(4)router.base:应用的根URL,此路径必须和外网访问的基本路径一致, 否则会导致nuxt自动跳转404页面。
举例:假设外网访问根路径为http://host/pathA/pathB/xxx,其中pathA为网关配置的应用区分路径,不可缺少;pathB为我们配置的nginx代理关键词,那么我们服务的router.base应为/pathA/pathB/,nginx的配置应为
location ^~ /pathB/ { proxy_pass http://127.0.0.1:port/pathA/pathB/; }
3、修改ecosystem.config.js
(1)script:pm2启动的入口,一般是服务器文件,server/index.js,注意这两个文件的路径需要做好对应
(2)cwd:项目启动的路径,一般为当前路径
(3)exec_mode:如果解决了cluster下的EBIG的问题,cluster较好
(4)instances:pm2创建的实例数量,如果使用fork,只能为1,否则需要做负载均衡;使用cluster,则可以为"max"
4、准备需要上传到服务器的文件,必须文件如下:
(1)nuxt.config.js:nuxt配置文件
(2)ecosystem.config.js:pm2配置文件
(3)package.json:入口文件
(4)server/:服务器入口文件,pm2的script配置的启动入口即此文件
(5).nuxt:nuxt build生成的文件夹,与nuxt.config.js中定义 的buildDir一致
(6)其余为nuxt.config.js里引用到的项目内文件
5、关于上传到cdn上的文件:
需要上传到cdn的是nuxt构建的nuxt-dist/dist/client/下的部分,上传到cdn之后,需要修改nuxt.config.js里的build.publicPath,对应此目录在cdn的路径