導言
去年有寫一篇關於構建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的路徑