記一次慘痛的Vue-cli + VueX + SSR經歷

前言介紹html

此篇寫於一年前,當時僅做爲本身的我的項目總結,如今換了工做,就把以前的一些經驗或教訓發出來,以警後人,也爲你們碰到相同問題時提供解決方案,或多或少有幫助您就點個贊,若是有問題或更好的解決方式請在評論中指出或關注公衆號給我留言,感謝指點。前端

總部提出新項目,大體需求就是APP內置一個H5商城,因而開始出差去總部極限開發,可沒想到碰到的問題讓我一個工做經驗只有半年多點的應屆生熬了好幾宿。node

技術選型

  • 項目語言:HTML、CSS、JavaScript
  • 項目框架:Vue.js
  • 項目搭建腳手架:Vue-cli
  • 工程化工具:Webpack、Sass、Npm
  • 源碼管理:Gitlab
  • 運行環境:Browser & Node(PM2)
  • 第三方服務:GrowingIO、高德地圖、ECharts

技術方案

  • 先後端分離
  • 非單頁面應用,多頁面站點,評估時大概30多個頁面,極限開發,直接使用 Vue-cli
  • 產品考慮SEO,使用 VueXSSR ( VueSSR 後續我會出文章說明實現方式和一些坑,有興趣的能夠點擊關注獲取最新文章)
  • 便於開發,使用 sass 等工程化工具
  • 肯定開發規範和代碼規範
  • 根據項目須要,封裝 user.js(針對用戶信息存儲), base.js(基礎公共JS文件), url.js(針對url的操做), http.js(二次封裝axios)
  • 根據項目需求,抽象組件(compoments)和插件(widgets)註冊到Vue實例
  • 根據項目須要,合理設置RouterPage
  • 根據接口及環境須要,設置必要的環境變量和接口地址
  • 根據開發和生產須要,添加必要的依賴

以Node爲服務器版本項目架構

  • 整個網站的架構採用橫向分層,從上往下愈來愈抽象,引用關係由上至下,拒絕由下至上的引用。
    • 語言&環境
    • 框架層
    • 業務公共層
    • 應用層

語言&環境

  • 語言:
    • 採用 HtmlHTML5 控制各個模塊的結構;
    • 採用 Sass 作樣式的預處理;
    • 採用 ECMAScript 6 來開發邏輯和交互,而後經過 WebpackBabel 將高級版本的 JS 編譯成當下流行瀏覽器可以解析的 ECMAScript 5
  • 環境:
    • Web 前端的代碼主要運行在瀏覽器端,可是也能在 Node 環境運行,經過 Vue-ssr Node 端插件,一樣的前端代碼也能夠經過服務器端將 Html 渲染出來。
    • 正式的部署中,Node 的進程管理是經過 PM2(process manager 2),它能夠幫你檢查進程的健康狀況,並提供強大的接口,讓你很容易的瞭解 Node 在服務器中的運行狀況。

框架層

  • 框架層主要解決:算法、存儲、通信和 UI 4 大問題。
    • Vue
  • 核心框架採用 Vue 及其 Vue 系列插件:
    • Vue-ssr 服務器端渲染模塊
    • Vue-Router 路由模塊
    • Vuex 數據流模塊
  • 選擇Vue做爲核心框架的緣由:
    • Vue 更加輕量級
    • Vue 入門成本更低
    • Vue 中文社區比較多,中文文檔也翻譯的很好
    • VueGitHub 中對問題的回覆也很及時
    • Vue 語法更加忠於前端語言
    • Vue 的解決方案更加齊全

公共業務層

  • 主要解決站點的業務問題,例如:系統配置、獲取用戶信息、與後端接口的交互等。
  • Config: 公共環境變量相關
  • Buiness:
    • user.js 用戶信息相關
    • filter 業務相關filter
  • Plugins:
    • bsae.js 基礎公共方法庫(包括精準計算,手機號脫敏,格式化金額等等)
    • cookie.js 針對cookie操做公共方法
    • gotoapps.js hyBrid公共方法
    • h5toapp.js hyBrid公共方法
    • url.js 針對url操做公共方法
    • weixinShare.js 分享公共方法
  • Components: 公共組件
    • 篩選組件
    • 城市選擇組件
    • 商品展現組件
    • 頭部導航
    • 等等其餘組件
  • Widgets: 公共插件
    • 彈出Toast
    • 二次封裝 Swiper 滑動插件
    • 滑動加載插件
    • 多選插件
    • 等等其餘插件

應用層

  • 業務頁面代碼
    • 首頁推薦頁
    • 不少落地頁
    • 商品列表頁
    • 商品詳情頁
    • 商品管理頁
    • 消息發佈頁
    • 等等其餘頁面

以Node爲服務器版本存在的問題

項目提測後通過壓測發現node服務器版本在併發量大的狀況下出現內存溢出的問題,內存不斷上漲致使容器內存溢出服務暫停,服務器探針檢測到服務暫停後從新開始部署操做,致使站點出現502的狀況(因爲壓測報告包含前東家信息,因此這裏不給出壓測報告的數據了)。ios

內存溢出問題

  • 分析緣由
    • 高併發是node服務器的瓶頸,增長服務器端渲染後這個問題更加突出 參考資料
    • 服務器端渲染將數據大量存儲在內存中,致使頁面不銷燬內存沒法釋放,內存猛增
    • 代碼書寫不規範,致使部分代碼出現內存泄漏的狀況
  • 嘗試解決方案

PM2監控問題

  • 分析緣由
    • 公司服務器上探針檢測代碼健康,內存溢出致使pm2重啓時,健康檢測不經過致使項目重啓

多線程問題

  • 分析緣由
    • 爲應對內存溢出問題,增長了多線程和組件緩存以及頁面緩存,可是機器的CPU壓力和內存壓力也同時增大

服務器端渲染接口請求報錯

  • 分析緣由
    • 服務器端獲取傳參錯誤,致使部分接口請求報錯,頁面阻塞

服務器端內存分配不合理

  • 分析緣由
    • 因爲是測試環境,前端後端兩個項目在同一臺機器上,內存分配不合理,壓測大量的請求都積攢在前端,致使後端接口持續等待

服務器硬件準備不足

  • 分析緣由
    • 壓測均在測試環境進行,測試環境服務器自己配給不足,也沒有作負載均衡,請求量激增致使服務器扛不住了

部分解決方案技術實現

釋放因代碼產生的沒必要要內存消耗

  • 釋放因錯誤書寫致使的多餘內存消耗
// 1. 掛載的隱式變量
fun(e) {
	// JS 的變量提高將其掛載到全局
	bar = "this is a hidden global variable"; // 使用let進行聲明
}
// 2. 直接調用的外部構造函數
fun() {
    this.variable = "potential accidental global";
}
this.fun(); // 將直接執行外部構造函數改成new繼承建立
複製代碼
  • 釋放時間器或callback未銷燬產生的內存消耗
//1. 使用結束時候清除定時器
let timer = setInterval(() => {
    // do something
}, 1000);
clearInterval(timer)
//2. 使用結束清除回調
let element = document.getElementById('button');
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
複製代碼

開啓多線程

  • 利用node 的 cluster 模塊能夠建立共享服務器端口的子進程
  • 參考官方文檔
const cluster = require('cluster')
const numCPUs = require('os').cpus().length

if (cluster.isMaster) {
    console.log('Master is running');
    for (var i = 0; i < numCPUs; i++) {
        cluster.fork();
    }
    cluster.on('exit', function (worker, code, signal) {
        console.log('worker ${worker.process.pid} died');
    });
} else {
    app.listen(port, () => {
        console.log(`server started at localhost:${port}`)
    })
}
複製代碼

使用 LRU-Cache 管理緩存

  • 設置頁面緩存
// server.js
const LRU = require('lru-cache')
const microCache = LRU({
    max: 100, // 最大緩存的數目
    maxAge: 1000 // 過時時間
})
const isCacheable = req => {
    // 判斷是否須要頁面緩存
    if (req.url && req.url === '/') {
        return req.url
    } else {
        return false
    }
}

app.get('*', (req, res) => {
    const cacheable = isCacheable(req)
    res.setHeader('Content-Type', 'text/html')
    if (cacheable) {
        const hit = microCache.get(req.url)
        if (hit) {
            return res.end(hit)
        }
    }
    const errorHandler = err => {
        if (err && err.code === 404) {
            // 未找到頁面
            res.status(404).sendfile('./assets/error/500.html');
        } else {
            // 頁面渲染錯誤
            res.status(500).end('500 - Internal Server Error')
            console.error(`error during render : ${req.url}`)
            console.error(err)
        }
    }
    const context =  { url: req.url }
    renderer.renderToString(context, (err, html) => {
        if (err) {
            return errorHandler(err)
        }
        
        if (context.initialState && context.initialState.htmlHead) {
            res.write( indexHTML.head
                .replace('<!-- TITLE -->', context.initialState.htmlHead.title)
                .replace('<!-- METAS -->', context.initialState.htmlHead.metas)
                .replace('<!-- SCRIPTS -->', context.initialState.htmlHead.scripts)
                )
        }
        res.write(html);

        if (context.initialState) {
            res.write(
                `<script>window.__INITIAL_STATE__=${serialize(context.initialState, {
                    isJSON: true
                })}</script>`
            )
        }

        res.end(indexHTML.tail)
        // 設置當前緩存頁面的內容*/
        microCache.set(req.url, html)
    })
})
複製代碼

調整node內存大小的使用限制

  • 在 build 或者 運行node環境的時候進行更改
"build": "node --max_old_space_size=4096 build/build.js"
複製代碼

PM2進程監控策略

  • 提升監控閾值
pm2 start app.js --max-memory-restart 1024M
複製代碼

改進後的壓測數據

測試環境壓測數據

  • 壓力:兩千個併發持續半小時

測試環境壓測結果

  • 應用程序性能滿意度爲0.525(範圍在 0-1之間,1表示達到全部用戶均滿意)
  • 請求經過率99.5%,失敗率0.5%。
  • 半小時的壓測請求總數爲三萬,服務共重啓三次,平均請求完成時間 6996.15ms ,最大請求完成時間 601994ms ,最小請求完成時間 381ms。

生產環境壓測數據

  • 壓力:一萬個併發持續半小時

生產環境壓測結果

  • 壓測開始後瞬間服務器壓力猛增
  • 壓測10分鐘響應時間超過10秒
  • 壓測10分鐘到20分鐘期間服務器內存溢出,探針檢測到溢出自動重啓服務器,項目出現502狀況

最終解決方案

因爲開發時間緊張,在上述處理方案進行改進後,壓測的效果好了一些,可是仍是達不到理想的要求,因此最終咱們放棄使用node做爲服務器底層。nginx

  • 修改底層框架,去除 node 服務器端相關配置,改由打包後直接由 nginx 作路由轉發,再也不使用 node 服務器做爲底層分發服務器
  • 修改後的架構仍然維持了 node 服務器版本的絕大部分架構,去掉了部分再也不須要用到的依賴,並增長了 keep-alive 緩存,並將部分靜態支持JS文件改由 npm 依賴包 獲取,如 sha256 算法
  • 在服務器端,在純淨 node 鏡像 的基礎上增長 Nginx ,並配置,將打包的過程放在 node 服務器上,打包成功後直接由 nginx 代理轉發

Nginx版本架構

語言&環境

  • 僅僅是環境層面的變化,將以node做爲服務器改成開發使用node服務器,生產測試均使用node打包,利用Nginx進行轉發,打包配置更加簡潔

框架層

  • 核心框架層仍然採用Vue,只是去除了VueX,Vue-SSR,增長使用了keep-alive進行頁面緩存
  • 再也不累贅過多的VueX進行狀態存儲與組件通信

公共業務層

  • 公共業務層不作修改,僅僅是將代碼層級的錯誤引用和內存消耗修改掉

應用層

  • 應用層不作修改

Nginx版本項目壓力測試報告

  • 性能優異,測試環境和生產環境壓力測試未見異常,壓測經過

項目總結(這裏只說須要改進問題)

需求產生階段合理規劃

  • 因爲本次開發很急,因此前端團隊中只有我和另外一個同事有 Vue 的熟練使用經驗,這是前端團隊去到總部以後萬萬沒有想到的,需求以及工期肯定下來以後向北京方面搖人,獲得的回答是北京方面需求量很大,過不去人了,索性咱們三我的硬是頂着巨大任務量一邊 coding 一邊向北京方面要人。
  • 因爲是先後端並行開發,一些先後端聯調上的問題在前期並無溝通好,致使開發階段 mock數據 徹底不能用,簡直是shit,後端同窗並不知道咱們要的是符合真實數據結構的數據。
  • 開發在總部,服務器和運維在北京,致使先後端的開發開始並不知道服務器的狀況,出現問題以後只能遠程和北京團隊商討解決方案,運維人員對項目狀況不瞭解,致使服務器物料準備不足。

規範化Coding , 規範化協做

  • 在項目初期就指定了一系列代碼規範,可是成員來自各個團隊,水平不一,缺乏嚴謹的 code review ,致使不少問題在開發階段就已經產生。
  • 團隊協做須要指定流程,不然團隊成員都忙於開發,無人在上層負責規劃。
  • 缺乏必要的單元測試,項目初期規劃使用 jest 進行單元測試,但繁重的開發任務致使沒有時間和精力進行。

合理分配時間

  • 我的認爲技術研發是一項很嚴謹的工做,在完成基本開發任務的前提下要留有充足的修改和思考的時間,每一個模塊,每一個小的需求點完成後要及時思考和總結,問題早發現、早診斷、早治療。

寫在最後

  • 項目總結是通過處理以後發出來的,裏面涉及到前公司相關的我都刪除掉了,可能有的解決方案並無完整展現給你們
  • 項目中存在的問題你們或多或少以前或者之後碰到,僅僅給你們提供一個解決方式和處理建議,若是想了解更多細節和解決方案,歡迎評論或者關注公衆號後臺留言,我看到都會回覆的
  • 此次項目給本身的技術成長是天翻地覆的,雖然本身並無總體負責這個項目,可是過程全程參與,其中苦樂酸甜嚐盡。
  • 如今這個項目還在運行,上線初期也是頂住了流量壓力,雖然過程很艱辛可是結果仍是好的。
  • 年前疫情還未開始的時候我已經換了工做,來到了熊廠,期間面試了一週,收到了三個offer,後續我會將本次面試過程的面試題發出來,你們敬請期待。

若有問題,請評論指正。碼字不易,點個贊再走唄!git

相關文章
相關標籤/搜索