千里之行,始於足下。古人老是言簡意賅地闡述樸實的道理。不少關於夢想關於計劃的事情,老是缺乏一個開始,而後又在開始以後缺乏一個堅持,最終夭折,無疾而終。好在咱們有了一個開始,並慢慢堅持了下來。javascript
隨着開發進度的持續推動,終於,咱們也要面臨項目部署上線的各類問題。怎樣持續迭代,怎樣控制代碼質量,怎樣發佈,怎樣保證應用在線上的穩定運行……又是茫茫多的問題搞得人慾仙欲死呀~css
首先來看一下 Nuxt文檔的命令和部署章節:前端
nuxt build
利用webpack編譯應用,壓縮 JS 和 CSS 資源(發佈用)nuxt start
以生產模式啓動一個Web服務器 (nuxt build 會先被執行)同時,在咱們初始化項目的 package.json 裏有以下指令配置:java
{ "build": "nuxt build", "start": "cross-env NODE_ENV=production node server/index.js", } 複製代碼
而後,咱們在終端執行 npm run build
指令,會獲得以下目錄結構的打包資源:node
├─ .nuxt/
│ ├─ components/
│ ├─ dist/
│ ├─ views/
│ ├─ App.js
│ ├─ client.js
│ ├─ ...
│ └─ ...
複製代碼
最後執行 npm run start
指令,終端告訴咱們:Server listening on http://localhost:3000
,瀏覽器中能夠看到,與咱們在開發階段執行 npm run dev
看到的畫面將別無二致:webpack
這一階段,程序將調用 nuxt.config.js | .nuxt/ | server/ | static/
等文件(夾)下的代碼來支撐整個應用的運行。ios
此時,整個應用愉快地運行在咱們地開發機上,一派欣欣向榮的景象呀!but……此時,咱們點了一個超連接,來到了一個新的頁面,頁面背後的程序在發生各類「化學反應」的同時遇到了一個 bug,因而整個服務崩掉了~~~nginx
試想一下,假如這是在生產環境發生的事件,大概是大型驚悚片現場足可媲美的畫面吧。畢竟咱們沒法保證可以徹底 catch 掉全部可能發生的錯誤,因此必需要解決掉單線程的 node 自己在必定程度上的不穩定性。固然 node 發展至今,生態以內早已有很多成熟的解決方案,要否則可能在多年之前早就能夠打出 GG 離場了吧,也輪不到我等新人在這裏「指手畫腳」呀。因此咱們在項目中選擇 pm2 來守護進程,以確保程序在線上的相對穩定。git
值得一提的是,我常常關注 Awesomes 站內一個叫 前端 TOP 100 的排行,裏面網羅了時下流行的或頗具熱度的前端技術。目前,Nuxt 排名第 94 位,而 pm2 排名第 56 位,而在一年前,二者都並未位列其中。先不談這個排行的準確性權威性,但至少足以說明一些問題和趨勢。ok,繼續回到 pm2~github
先扔一個 pm2 的傳送門。能夠看到首頁上醒目地寫着一行指令:npm install pm2 -g
,彷彿在大聲告訴咱們:「我能幫你管理你運行在線上的應用並讓它活得好好的,快來全局安裝我,幫你守護進程吧~」。
ok,照作。完畢後慣例 pm2 -v
查看安裝結果。而後,終端來到咱們的項目目錄下,確保已經執行了 npm run build
指令併成功獲得了上文提到的打包資源,運行 pm2 start npm -- run start
,就能夠將咱們的 Nuxt 應用置於 pm2 的掌控之下了。
如上圖所示,pm2 幫咱們啓動了一個名爲 npm 的 node 進程,模式爲 fork
,接下來幾項分別是版本號、pid、上次更新時間和重載次數、運行狀態、cpu和內存使用狀況、是否監控項目下的文件變動等信息,以上的大部分信息咱們均可以經過 cli 輸入相應的指令進行控制。好比經過 pm2 start npm --name mynuxtdemo -- run start
,就能夠啓動一個名爲 mynuxtdemo 的應用(爲啥不直接 pm2 start ./server/index.js
呢,遍歷文檔,彷佛沒法經過 cli 設置環境變量,不會是俺年紀大了眼神兒很差吧...),如圖:
下面來看一下幾個經常使用的配置項:
# 給你的應用起個名兒吧 --name <app_name> # 監控項目下的文件變動,一旦出現變動,重啓 --watch # 設置一個容許當前應用佔用的內存上限,一旦超過了,重啓 --max-memory-restart <200MB> # 設置日誌的輸出路徑 --log <log_path> # 爲腳本傳遞額外的參數 -- arg1 arg2 arg3 # cluster模式下開啓示例的數量,當設置爲 max 時,根據當前主機的 cpu 核數設置 -i <instance_num> 複製代碼
最終,咱們能夠經過 cli 指令 pm2 start npm --watch -i max --name mynuxtdemo -- run start
來啓動應用。以個人電腦爲例,將啓動一個名爲 mynuxtdemo 的 Nuxt 應用,pm2 調用了 node 的 cluster 模塊,根據 cpu 核數開啓了 8 個實例,同時 pm2 將監聽當前項目下的文件,當文件出現變動時,pm2 會嘗試重啓全部的 8 個實例以實現更新。
而後咱們來看一下在程序意外終止的狀況下會發生什麼:
此時,我在 server/index.js
下拋出了一個自定義異常 throw 'this is a test error'
,能夠看到 pm2 在程序終止時在不斷嘗試重啓的過程。
ok,至此關於 pm2 的簡單介紹即將告一段落,可是每次須要啓動程序的時候都須要這樣一長串指令的輸入也是一件很痛苦的事情,至少對於我這種"老年人記性"是不太可以接受的~
還好 pm2 提供了配置文件的方式來描述咱們須要它做出何種行爲,以知足咱們的預期,就像 gulp | eslint ...
它們那樣,一樣提供了多種多樣的文件格式,總有一款符合你的口味。詳情見 PM2 Ecosystem File 章節。這裏,咱們新建了一個 pm2.config.json
文件來描述咱們對 pm2 提出的「要求」:
{ "apps": [ { "name": "mynuxtdemo", "script": "./server/index.js", "instances": 4, "max_memory_restart": "500M", "watch": [ "./server", "./nuxtdist" ], "ignore_watch": [ "./nuxtdist/dist/client" ], "env": { "NODE_ENV": "production", "DEMO_ENV": "prod" }, "env_tset": { "NODE_ENV": "production", "DEMO_ENV": "test" }, "env_dev": { "NODE_ENV": "development", "DEMO_ENV": "dev" }, "output": "./logs/out.log", "error": "./logs/error.log", "log_date_format":"YYYY-MM-DD HH:mm CCT", } ] } 複製代碼
而後,咱們在 package.json
中做以下配置:
{ "scripts": { "dev": "cross-env DEMO_ENV=dev NODE_ENV=development nodemon server/index.js --watch server", "build": "cross-env DEMO_ENV=prod NODE_ENV=production nuxt build", "start": "cross-env DEMO_ENV=prod NODE_ENV=production node server/index.js", "pm2start": "pm2 start pm2.config.json", "buildtest": "cross-env DEMO_ENV=test NODE_ENV=production nuxt build", "starttest": "cross-env DEMO_ENV=test NODE_ENV=production node server/index.js", "pm2test": "pm2 start pm2.config.json --env test" } } 複製代碼
從此,咱們只要執行 npm run pm2start
就能夠開始愉快地玩耍了~
固然了,上文所及僅是 pm2 的冰山一角,它還有不少的配置選項能夠知足咱們這樣那樣的需求,還請通讀文檔,並待往後帶着問題找答案吧。 下面列幾個平時經常使用的 pm2 指令:
pm2 list
pm2 logs
pm2 show 0
pm2 reload all
pm2 start 0
pm2 delete all
pm2 flush
#...
複製代碼
終於,在某一天,當前的開發進度漸漸接近尾聲了,咱們也要嘗試讓應用運行在生產環境下來一睹其風采了。畢竟對於咱們整個團隊來講,Nuxt 仍是一個陌生的框架,早作準備總沒有錯。
首先,咱們使用 nginx 來做反向代理,Nuxt 文檔很貼心地給處理一個示例代碼(好久之前彷佛是木有的?😢),戳 Nginx 使用nginx做爲反向代理 圍觀。爲 nginx 添加本項目的配置文件,好比在 nginx/conf.d/
下新建一個 mynuxtdemo.conf
文件,並有以下配置:
upstream nuxt_demo {
server 127.0.0.1:3000;
}
server {
listen 80;
server_name test.mynuxtdemo.com;
gzip on;
gzip_types text/plain application/xml text/css application/javascript;
gzip_min_length 1000;
location / {
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Nginx-Proxy true;
proxy_cache_bypass $http_upgrade;
proxy_pass http://nuxt_demo;
# ...
}
}
複製代碼
重啓 nginx 使配置生效,而後將打包後的資源(nuxt.config.js | .nuxt/ | server/ | static/
...etc)上傳至服務器,項目根目錄下npm i --production
,依賴安裝後,繼續執行 npm run pm2start
,而後在瀏覽器訪問 test.mynuxtdemo.com 就能夠訪問咱們的 nuxt 應用了。
此時,個人手裏有了一個生產環境下直達該項目根目錄的 ftp 帳號,用於項目迭代過程當中上傳新版本的打包產物。項目小分隊們繼續作正式上線前的開發工做,某一天在好幾個時間段完成了好幾個新的功能並伴隨着產品同窗永不停歇的改這改那的碎碎念,因而 npm run build >> ftp upload >> npm run build >> ftp upload ...
。更不能忍受的是,還要不停地把 .nuxt/dist/client/
下的資源上傳到阿里雲 oss 上。 當時個人心裏是絕望的,來回折騰,效率低下,還容易出錯,哭。
總之,遇到問題就要解決問題,此時腦海中閃過那句話:你遇到的 99% 的問題均可以在互聯網上找到答案。因而果斷投入 npm 和 github 的懷抱尋求解救,並順利找到答案。通過一番篩選,最終選擇了兩個插件來做爲輔助上傳的工具,分別是:ftp 客戶端 node 版本 和 阿里雲 oss 上傳插件。基於這兩個插件,並根據咱們現階段的需求,分別編寫了 ftp 和 oss 的上傳腳本:
// uploader/ftp.js const fs = require('fs'); const path = require('path'); const ftp = require('ftp'); const log = require('single-line-log').stdout; // ftp服務器參數配置 const ftp_config_hjxy = { host: '1xx.xxx.xxx.xxx', port: 21, user: "username", password: "password" } const topPath = '/nuxtdist'; // 服務器目標路徑(咱們的dist目錄配置爲 nuxtdist/) const dirpath = path.resolve(__dirname, '../nuxtdist'); // 本地須要上傳的文件夾路徑 let streamList = []; let mkDirList = []; let streamAmount = 0; streamFactory(dirpath, topPath); const c = new ftp(); c.connect(ftp_config_hjxy); //連接ftp服務器 c.on('ready', function (err) { //準備操做 if(err) throw err; console.log('\x1B[32m√\x1B[39m connection successful.'); // 先建立可能不存在的文件夾 mkDirList.forEach((dirPath) => { c.mkdir(dirPath, false, (err) => { err ? console.log(`\x1B[33m建立文件夾${dirPath}失敗--已存在/未知錯誤-${err}\x1B[0m`) : console.log(`\x1B[32m建立文件夾${dirPath}成功\x1b[39m`); }); }); // 而後開始上傳文件 streamList.forEach((fileItem, idx) => { c.put(fileItem.stream, fileItem.destPath, false, (err) => { if (err) { console.log(`上傳${fileItem.destPath}失敗${err}`); } log(`\x1B[32m文件已上傳-${idx + 1}/${streamAmount}-\x1b[39m`); (idx + 1 === streamAmount) && c.end(); }); }); }); c.on('error', function (err) { if(err) throw err; console.log("err", err); }); /** * 生成stream流列表 * @param {String} dirpath * @param {String} destPath */ function streamFactory(dirpath, destPath) { const files = fs.readdirSync(dirpath); files.length && files.forEach((filename) => { const filepath = path.resolve(dirpath, filename); const isDir = fs.statSync(filepath).isDirectory(); const isFile = fs.statSync(filepath).isFile(); if(isFile) { const stream = fs.createReadStream(filepath); streamList.push({ stream, destPath: `${destPath}/${filename}` }); streamAmount++; }else if (isDir) { if(filename === 'client') return; mkDirList.push(`${destPath}/${filename}`) streamFactory(filepath, `${destPath}/${filename}`); } }); } 複製代碼
這裏有一個小 bug,服務器中 topPath 的目錄必須存在。因爲除了打包產出的資源之外的文件都相對穩定,而且
server/
下的代碼更新須要格外謹慎,因此這裏只作打包資源的上傳,並剔除了.nuxt/dist/client/
下的文件。接下來就須要把.nuxt/dist/client/
下的文件上傳至 oss 了,思路上與上面 ftp 上傳的代碼大同小異。
// uploader/oss.js const OSS = require('ali-oss'); const fs = require('fs'); const path = require('path'); const log = require('single-line-log').stdout; const client = new OSS({ region: 'your oss region', accessKeyId: 'your oss accessKeyId', accessKeySecret: 'your oss accessKeySecret', bucket: 'nuxtdemo', }); const aliyunOss = { bucket: 'nuxtdemo', site: 'your site addr', dirName: '/nuxtclient' }; const dirpath = path.resolve(__dirname, '../nuxtdist/dist/client'); let streamList = []; let streamAmount = 0; streamFactory(dirpath, aliyunOss.dirName); streamList.forEach((fileItem, idx) => { putStream(fileItem.destPath, fileItem.stream); log(`\x1B[32m--正在上傳${idx + 1}/${streamAmount}--\x1B[39m`); (idx + 1 === streamAmount) && log('\x1B[32m上傳完成\x1B[39m'); }); /** * aliyunoss 流式上傳 * @param {String} filename 上傳至oss使用的文件名 * @param {String} stream 可讀的文件流 */ async function putStream (filename, stream) { try { await client.putStream(filename, stream); } catch (err) { throw err; } } /** * 生成stream流列表 * @param {String} dirpath 本地須要上傳的目錄位置 * @param {String} destPath 上傳至服務器的目錄位置 */ function streamFactory(dirpath, destPath) { const files = fs.readdirSync(dirpath); files.length && files.forEach((filename) => { const filepath = path.resolve(dirpath, filename); const isDir = fs.statSync(filepath).isDirectory(); const isFile = fs.statSync(filepath).isFile(); if(isFile) { const stream = fs.createReadStream(filepath); streamList.push({ stream, destPath: `${destPath}/${filename}` }); streamAmount++; }else if (isDir) { streamFactory(filepath, `${destPath}/${filename}`); } }); } 複製代碼
而後,咱們在 package.json
中增添以下兩條新的配置:
{ "script": { "oss": "node uploader/oss.js", "ftp": "node uploader/ftp.js" } } 複製代碼
這樣,只須要在打包完畢後,分別執行 npm run oss
和 npm run ftp
就能夠將新的代碼安排到服務器了。
考慮到常常要進行 oss 上傳,因而又將 oss.js
部分封裝成一個 npm oss上傳插件,這樣之後只須要在須要用到的地方 npm install --save-dev @crazymuyang/alioss-uploader
安裝,並增長一個簡單的配置文件就可使用了:
const aliOssUploader = require('@crazymuyang/alioss-uploader'); const path = require('path'); const aliossConfig = { region: 'your oss region', accessKeyId: 'your oss accessKeyId', accessKeySecret: 'your oss accessKeySecret', bucket: 'your oss bucket', }; const uploadConfig = { dirpath: path.resolve(__dirname, './test'), // 將該路徑下的文件上傳至oss() destpath: '/test', // 將文件上傳至bucket下的該路徑下 } const uploader = new aliOssUploader(aliossConfig, uploadConfig); uploader.start(); 複製代碼
就這樣,跌跌撞撞中項目能夠勉強上線了。然而,咱們發如今 ftp 上傳的過程當中會進入長時間的 502 狀態,此時 pm2 的 watch 功能不斷監測到文件變動,同時不斷地嘗試重啓實例,直到最後一個文件上傳完畢,整個應用恢復正常。此時,咱們執行 pm2 ls
查看會發現 reload 的次數增長了好屢次。
這樣確定是不行的呀,因而便採用了一個折中的辦法,爲 nginx 配置 502 跳轉頁面(loading),而後在這個頁面裏經過定時器在一段時間後再跳回到項目域名下。此時,在文件上傳過程當中就是一個路由來回重定向的過程,用戶視角下就是有一段時間應用一直停留在一個 loading 頁面。這樣有一個很明顯的弊端,從 loading 頁面回來只能去往首頁,而沒法跳回以前用戶訪問的頁面。
因此,問題來了:到底怎樣才能實現真正地不停機更新呢?其實答案很簡單,之因此會長時間處於 502 狀態,僅僅是由於短期內 pm2 監聽了大量文件變動,它跟不上趟了。
如何解決,以實現真正的不停機呢?且聽下回分解~