<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于Vite2+Vue3的項目復盤總結

          共 32392字,需瀏覽 65分鐘

           ·

          2021-06-23 18:56

          (給前端大學加星標,提升前端技能.

          作者:不燒油的小火柴

          https://juejin.cn/post/6969758357288648718

          1.項目背景與技術選型

          1.1 項目背景

          我們要做一個基于邊緣計算的物聯(lián)網管理平臺雛形,流程大概是這樣:數(shù)據(jù)(傳感器收集通過串口傳入邊緣節(jié)點服務器)經過邊緣節(jié)點計算處理后上傳到云端,然后再經過一些處理后得到一個數(shù)據(jù)倉庫。用戶可以基于這個數(shù)據(jù)倉庫可以對設備狀態(tài)、健康度進行可視化管理??偟膩碚f就是要做三套系統(tǒng):

          • 邊緣節(jié)點設備管理系統(tǒng)
          • 云端管理系統(tǒng)
          • 大屏可視化系統(tǒng)

          業(yè)務聽起來高大上,但是對于我來說實際上還是增刪改查??,大屏可視化稍微有些難點。

          那問題來了:簡單的增刪改查項目如何搞出花來呢,那么就涉及到接下來的技術選型了。

          1.2 技術選型

          我覺得技術選型一定得從需求和業(yè)務出發(fā),這是最重要的,不能為了使用新技術而使用。雖然說這么說,但我還是選擇了比較新的技術,原因是我們這套業(yè)務系統(tǒng)算是一個產品雛形,也不需要兼容任何版本IE瀏覽器,產品的周期與穩(wěn)定性都還不確定,而且我們要在很短的時間內做出一個雛形,另外我確實想把剛學的技術實踐一下,不然確實會忘了噢??。

          基于這樣的考慮,我選擇了當時發(fā)布不久(剛發(fā)布一周不到)的Vite2.0作為我們項目的腳手架,選擇Vue3.0進行開發(fā),UI框架是還處于Beta版本的Element-Plus,甚是刺激!我簡單畫了一張圖:

          image.png

          接下來對項目涉及到的知識進行梳理與總結,我希望從這個“增刪改查”項目學到一些不一樣的知識,如果對您也有用,那就更好啦??。

          2.基礎設施搭建

          在講述前,先把我畫的線框圖呈上,方便對整個項目的基礎設施有個大概的了解:

          image.png

          Vite創(chuàng)建Vue3.0項目很簡單,一行命令可以搞定:

          yarn create @vitejs/app my-vue-app --template vue

          # 如果需要交互式命令創(chuàng)建
          yarn create @vitejs/app
          復制代碼

          具體細節(jié)可以參考文檔。

          創(chuàng)建出來的目錄很簡單,但是這遠遠不夠啊,接下來我們需要把一些重要的基礎設施搭起來。

          2.1 缺陷管理

          當時也是剛學完,不知道如何搭建一個項目,但是有一個是必須要去做的:團隊開發(fā)規(guī)范。我采用的是業(yè)內比較成熟的方案:

          • 語法風格檢測:ESLint+Prettier
          • Git Message提交規(guī)范:Angular提交規(guī)范

          2.1.1 代碼風格約束

          我們先來看看在Vue3.0的項目中如何使用ESLintPrettier對代碼風格進行約束。

          首先安裝這幾個包:

          • eslint:代碼質量檢測(用var還是let,用==還是===...)
          • prettier:代碼風格檢測(加不加尾逗號,單引號還是雙引號...)
          • eslint-config-prettier:解決ESLint與Prettier的風格沖突
          • eslint-plugin-prettier:ESLint的插件,集成Prettier的功能
          • eslint-plugin-vue:ESLint的插件,增加Vue的檢測能力
          yarn add eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue -D
          復制代碼

          接下來在項目根目錄下創(chuàng)建兩個文件.eslintrc.jsprettier.config.js(拓展名可以自由選擇),我們把下面內容加進去:

          .eslintrc.js

          module.exports = {
            parser'vue-eslint-parser',
            env: {
              browsertrue,
              nodetrue,
              es2021true
            },
            extends: ['plugin:vue/vue3-recommended''plugin:prettier/recommended'],
            parserOptions: {
              ecmaVersion12,
              sourceType'module'
            },
            rules: {
              'prettier/prettier''error'
            }
          }
          復制代碼

          prettier.config.js

          module.exports = {
            printWidth100,
            tabWidth2,
            useTabsfalse,
            semifalse// 未尾逗號
            vueIndentScriptAndStyletrue,
            singleQuotetrue// 單引號
            quoteProps'as-needed',
            bracketSpacingtrue,
            trailingComma'none'// 未尾逗號
            arrowParens'always',
            insertPragmafalse,
            requirePragmafalse,
            proseWrap'never',
            htmlWhitespaceSensitivity'strict',
            endOfLine'lf'
          }
          復制代碼

          然后使用ctrl+shift+P調出控制臺輸入Reload Window配置即可生效,以后想拓展代碼的風格都在prettier.config.js進行配置,而代碼語法相關規(guī)則在.eslintrc中的rules配置即可。相關規(guī)則文檔:

          • ESLint Rules
          • Prettier中文網

          2.1.2 Git提交約束

          接下來我們需要對我們的提交進行約定,先安裝一下這幾個包:

          • husky:觸發(fā)Git Hooks,執(zhí)行腳本
          • lint-staged:檢測文件,只對暫存區(qū)中有改動的文件進行檢測,可以在提交前進行Lint操作
          • commitizen:使用規(guī)范化的message提交
          • commitlint: 檢查message是否符合規(guī)范
          • cz-conventional-changelog:適配器。提供conventional-changelog標準(約定式提交標準)?;诓煌枨?,也可以使用不同適配器(比如: cz-customizable)。
          yarn add husky lint-staged commitizen @commitlint/config-conventional @commitlint/cli  -D
          復制代碼

          先配置適配器:

          # yarn
          npx commitizen init cz-conventional-changelog --yarn --dev --exact

          # npm
          npx commitizen init cz-conventional-changelog --save-dev --save-exact
          復制代碼

          它會在本地項目中配置適配器,然后去安裝cz-conventional-changelog這個包,最后在package.json文件中生成下面代碼:

           "config": {
              "commitizen": {
                "path""cz-conventional-changelog"
              }
            }
          復制代碼

          現(xiàn)在我們可以添加一個腳本就可以編寫規(guī)范化的提交:

          "scripts": {
              "cz""git cz"
            }
          復制代碼

          接下來你可以執(zhí)行yarn cz命令來編寫一些約定好的提交規(guī)范:

          image.png

          此時我們已經根據(jù)約定規(guī)范提交了消息,但是我們怎么知道提交的消息是不是正確的呢,那么接下來就需要配置剛剛介紹到的commitlint,只需要一句命令即可完成配置,它會在項目根目錄下面創(chuàng)建一個commitlint.config.js配置文件:

          echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js
          復制代碼

          它會使用@commitlint/config-conventional這個包里面提供的校驗規(guī)則進行校驗,你可以理解為ESLint的規(guī)則。

          有了這個校驗工具,怎么才可以觸發(fā)校驗呢,我們希望在提交代碼的時候就進行校驗,這時候husky就可以出場了,他可以觸發(fā)Git Hook來執(zhí)行相應的腳本,而我們只需要把剛剛的校驗工具加入腳本就可以了,我們使用的是6.0的新版本,下面是具體使用方法:

          我們需要定義觸發(fā)hook時要執(zhí)行的Npm腳本:

          • 提交前對暫存區(qū)的文件進行代碼風格語法校驗
          • 對提交的信息進行規(guī)范化校驗
            "scripts": {
              "lint-staged""lint-staged",
              "commitlint""commitlint --config commitlint.config.js -e -V"
            },
            "lint-staged": {
              "src/**/*.{js,vue,md,json}": [
                "eslint --fix",
                "prettier --write"
              ]
            }
          復制代碼

          接下來就是配置husky通過觸發(fā)Git Hook執(zhí)行腳本:

          # 設置腳本`prepare`并且立馬執(zhí)行來安裝,此時在根目錄下會創(chuàng)建一個`.husky`目錄
          npm set-script prepare "husky install" && npm run prepare

          # 設置`pre-commit`鉤子,提交前執(zhí)行校驗
          npx husky add .husky/pre-commit "yarn lint-staged"

          # 設置`pre-commit`鉤子,提交message執(zhí)行校驗
          npx husky add .husky/commit-msg "yarn commitlint"
          復制代碼

          此時已經完成配置了,現(xiàn)在團隊里面任何成員的提交必須按照嚴格的規(guī)范進行了。

          2.1.3 IDE環(huán)境約束

          除此之外,我們還統(tǒng)一了VSCode編碼環(huán)境,通過Setting Sync插件使用Public Gist進行同步。

          有人會說小團隊做這個有必要嗎?我實踐了幾個月后,我個人還是覺得很有必要的,雖然剛開始配置起來很麻煩,也踩了不少坑,但實際去執(zhí)行這套流程其實不需要花太多時間,至少可以在開發(fā)階段避免除了代碼邏輯以外的錯誤。

          2.2 持續(xù)集成(CI)

          持續(xù)集成是自動化流程中一個十分重要的部分,我們的前端應用在傳統(tǒng)部署模式下需要自己打包然后上傳服務器,這樣很麻煩也很浪費時間。而持續(xù)集成就幫我們解決了這個問題,我們只要開發(fā)一個功能,它就能上傳到線上的制品庫,再配合持續(xù)部署(CD)就能夠提高我們的效率,讓我們專注開發(fā)。這一套流程需要有以下技術或平臺支撐:

          • Docker(容器化技術)
          • Linux
          • Nginx:高性能Web服務器
          • Jenkins:持續(xù)構建平臺
          • GitLab(本地部署的倉庫)
          • Nexus3:用來部署Npm、Docker私有倉庫,提供鏡像制品庫

          由于篇幅原因,上述平臺的搭建我不會給大家演示了。主要是給大家說一下這個CI流程:

          • 開發(fā)功能
          • Git提交到本地GitLab
          • GitLab觸發(fā)Webhook
          • Jenkins開發(fā)執(zhí)行腳本構建成Docker鏡像
          • 上傳Nexus私有倉庫
          image.png

          現(xiàn)在有了制品倉庫就需要持續(xù)部署(CI),但是我技術能力不夠,只能借助Jenkins執(zhí)行腳本來實現(xiàn),但是原則上來說在Docker容器里面部署Docker容器這樣的做法并不好,很容易出現(xiàn)一些問題,我想學習完Kubernetes后再來對這個流程進行優(yōu)化。

          搭建完流程后,我們只需要在項目中寫好DockerfileNginx的配置文件就可以了,下面是我項目中的一個案例:

          Dockerfile

          # build stage
          FROM node:lts-alpine as build-stage
          WORKDIR /app
          COPY . .
          RUN yarn && yarn build

          # production stage
          FROM nginx:stable-alpine as production-stage
          COPY --from=build-stage /app/dist/ /usr/share/nginx/html/
          COPY --from=build-stage /app/nginx.conf /etc/nginx/conf.d/default.conf
          EXPOSE 80
          CMD ["nginx""-g""daemon off;"]
          復制代碼

          nginx.conf

          server {
            listen 80;
            server_name localhost;

            #charset koi8-r;
            access_log /var/log/nginx/host.access.log main;
            error_log  /var/log/nginx/error.log  error;

            location / {
              root /usr/share/nginx/html;
              try_files $uri $uri/ @router;
              index index.html;
              expires -1;
            }

            location @router {
              rewrite ^.*$ /index.html last;
            }

            # SPA應用的history模式路由需要在前端配置500、400錯誤
          }
          復制代碼

          這里涉及到很多知識,篇幅有限,就不詳細說了。

          2.3 CSS樣式管理

          由于 Vite 的目標僅為現(xiàn)代瀏覽器,因此建議使用原生 CSS 變量和實現(xiàn) CSSWG 草案的 PostCSS 插件(例如 postcss-nesting)來編寫簡單的、符合未來標準的 CSS。

          官方其實是建議使用CSS變量來編寫CSS,但是考慮到現(xiàn)階段大家用得不是很熟練,所以還是采用了Sass,而且腳手架已經內置了對Sass的支持,我們只需要安裝即可,不用像Webpack那樣需要先安裝Loader。

          yarn add sass -D
          復制代碼

          這樣我們在模板里面的只要給<style>標簽加上lang就可以了:

          <style lang="scss" scoped>
          // ...
          </style>
          復制代碼

          我是按照下面的文件組織來對CSS進行統(tǒng)一管理的:

          src/assets/styles:

          • variables.scss(存放全局Sass變量)
          • mixins.scss(mixin)
          • common.scss(公共樣式)
          • transition.scss(過渡動畫樣式)
          • index.scss(導出上面三個樣式)

          index.scss

          @import './variables.scss';
          @import './mixins.scss';
          @import './common.scss';
          @import './transition.scss';
          復制代碼

          然后在main.js導入index.scss就可以使用了:

          import '/src/assets/styles/index.scss'
          復制代碼

          但是這里會有一個坑,那就是我在variables.scss中定義的變量、在mixins.scss定義的mixin全部失效了,而且控制臺也報錯:

          image.png

          如果不使用這個變量,我在Chrome是可以看到其他樣式已經被編譯好的,所以我采取了第二種方式導入index.scss。我們需要在vite的配置文件給css的預處理器進行配置,它的使用方式和Vue CLI中的配置差不多:

          vite.config.js

          export default defineConfig({
            plugins: [vue()],
            css: {
              preprocessorOptions: {
                scss: {
                  additionalData`@import "src/assets/styles/index.scss";`
                }
              }
            }
          })
          復制代碼

          這樣就完成了CSS的管理啦,當然我這種比較簡單,現(xiàn)在還有一種比較新的用法就是使用CSS Module,希望將來能用上吧。

          Sass的編寫指南,大家可以參考一下:Sass Guidelines

          2.4 接口管理

          2.4.1 基于Axios二次封裝

          基于Axios二次封裝已經是一種常規(guī)操作了,下面來看看項目中我是如何對API進行管理的:

          • 抽象一個HttpRequest類,主要有請求攔截/取消、REST請求(GET、POST、PUT、Delete)、統(tǒng)一錯誤處理等功能
          • 實例化這個類,然后分模塊編寫API請求函數(shù),URL這種常量單獨放一個文件,接口請求參數(shù)和請求體由使用者決定,最后導出一個對象出口
          • 在組件引入對應的模塊即可使用

          Vue3.0中最推薦的使用方式是Composition API,組件中this不推薦使用,所以如果想全局引入,需要這么做:

          import { createApp } from 'vue'
          import App from './App.vue'
          import http from '@/api'

          const app = createApp(App)
          app.config.globalProperties.http = http
          app.mount('#app')
          復制代碼

          在組件中使用:

          import { getCurrentInstance } from 'vue'

          const {
              proxy: { http }
            } = getCurrentInstance()

          // demo...
          async fetchData() {
           await http.getData(...)
          }
          復制代碼

          而之前在Vue2中我們只需要在Vue.prototype上定義屬性,然后在組件中使用this引入就可以了。但是全局引入會導致Vue原型很臃腫,每個組件的實例都會有這個屬性,會造成一定的性能開銷。

          Vue3這種全局引入的做法我覺得也很麻煩,所以我的做法是在使用的組件中導入對應的API模塊。

          打個小廣告:詳細的Axios封裝可以參考我的另外一篇文章在Vue項目中對Axios進行二次封裝。

          2.4.2 載入不同模式下全局變量

          此外,我們也可以通過使用.env文件來載入不同環(huán)境下的全局變量,Vite中也使用了 dotenv來加載額外的環(huán)境變量,設置的全局變量必須以VITE_為前綴才可以正常被加載,使用方式如下:

          .env.development

          # 以下變量在`development`被載入
          VITE_APP_BASE_API = '/api/v1'
          復制代碼

          .env.production

          # 以下變量在`production`被載入
          VITE_APP_BASE_API = 'http://192.168.12.116:8888/api/v1'
          復制代碼

          全局變量使用方式:

          import.meta.env.VITE_APP_BASE_API
          復制代碼

          2.4.3 跨域問題

          Vite是基于Node服務器開發(fā)的,所以它也提供了一些配置來實現(xiàn)本地代理,使用方式大家應該很熟悉,這里直接上一個例子:

          vite.config.js

          server: {
              opentrue,
              proxy: {
                '/api/v1/chart': {
                  target'http://192.168.12.116:8887',
                  changeOrigintrue
                },
                '/api/v1': {
                  target'http://192.168.12.116:8888',
                  changeOrigintrue,
                  rewrite(path) => path.replace(/^\/api/v1, '')
                }
              }
            }
          復制代碼

          如果線上的服務器后端服務器不是同源部署也會有跨域問題,那么需要在Ngnix中配置反向代理,好在后端實現(xiàn)了CORS規(guī)范,那我們不需要操心線上的跨域問題了。當然解決跨域的方式有很多,下面要介紹的WebSocket就沒有這個問題。

          到這里接口管理相關問題也差不多說完了,項目接口數(shù)量并不是特別多,就20多個吧,所以并沒有把全部接口全部交給Vuex接管,只有一少部分組件依賴的全局狀態(tài)才放到Vuex中。

          2.4.4 WebSocket+Vuex狀態(tài)管理方案

          大屏項目有將近20多個圖表都是實時數(shù)據(jù),包括設備健康度狀態(tài)、設備運行指標等等,必須使用WebSocket。但是我們項目是SPA應用,每個組件都需要發(fā)消息,并且需要共享一個WebSocket實例,跨組件通信很麻煩,所以需要對這一塊進行封裝。

          網上找了很多方案都沒有解決我的問題,但是偶然卻翻到了一個大佬的文章(websocket長連接和公共狀態(tài)管理方案),他的文章里提到了基于WebSocket+Vuex實現(xiàn)“發(fā)布-訂閱”模式對全局組件狀態(tài)進行統(tǒng)一管理。我看完后受益匪淺,我這才知道如果把設計模式用于開發(fā)中能有如此“功效”。

          我基于大佬的封裝又優(yōu)化了一些:

          • 對一些代碼細節(jié)進行了修改和優(yōu)化
          • 使用Class語法糖增強代碼可讀性
          • 增加了數(shù)據(jù)分發(fā)的功能

          下面是一個簡單的圖:

          image.png

          組件通過emit方法來發(fā)送消息,消息里面標識了任務名,后端返回的數(shù)據(jù)里面也會返回這個任務名,這就形成了一個“管道”。Vuex通過訂閱所有消息,然后根據(jù)任務名commit對應的mutation來完成狀態(tài)變更,最后組件通過Vuex的store或者getter就能拿到數(shù)據(jù)了。

          下面是一個完整的例子:

          首先封裝一個VueSocket類,它有發(fā)布、訂閱、斷線重連、心跳檢測、錯誤調度等功能。組件只需要通過emit方法來發(fā)布消息,通過subscribe方法訂閱服務端消息,然后通過Vuex的mutation來分發(fā)消息。

          我們目前只需要關注emitsubscribe兩個方法,當然handleData這個函數(shù)也很重要,主要是對來自不同任務的數(shù)據(jù)進行分發(fā)。

          src/utils/VueSocket.js

          class VueSocket {
            /**
             * VueSocket構造器
             * @param {string} url socket服務器URL
             * @param {function} commit Vuex中的commit函數(shù),提交mutation觸發(fā)全局狀態(tài)變更
             * @param {function} handleData 數(shù)據(jù)分發(fā)處理函數(shù),根據(jù)訂閱將數(shù)據(jù)分發(fā)給不同的任務處理
             */

            constructor(url, commit, handleData = null) {
              this.url = url // socket連接地址
              this.commit = commit
              this.distributeData = handleData

              this.ws = null // 原生WebSocket對象
              this.heartbeatTimer = null
              this.errorResetTimer = null // 錯誤重連輪詢器
              this.disconnectSource = '' // 斷開來源: 'close' 由close事件觸發(fā)斷開, 'error'由error事件觸發(fā)斷開
              this.reconnectNumber = 0 // 重連次數(shù)
              this.errorDispatchOpen = true // 開啟錯誤調度
              this.closeSocket = false // 是否關閉socket
              this.init()
            }

            /**
             * 錯誤調度
             * @param {string} type 斷開來源=> 'close' | 'error'
             * @returns {function}
             */

            static errorDispatch(type) {
              return () => {
                if (this.disconnectSource === '' && this.errorDispatchOpen) {
                  this.disconnectSource = type
                }

                console.log(`[Disconnected] WebSocket disconnected from ${type} event`)

                // socket斷開處理(排除手動斷開的可能)
                if (this.disconnectSource === type && !this.closeSocket) {
                  this.errorResetTimer && clearTimeout(this.errorResetTimer)
                  VueSocket.handleDisconnect()
                }
              }
            }

            /**
             * 斷開處理
             * @returns {undefined}
             */

            static handleDisconnect() {
              // 重連超過4次宣布失敗
              if (this.reconnectNumber >= 4) {
                this.reconnectNumber = 0
                this.disconnectSource = ''
                this.errorResetTimer = null
                this.errorDispatchOpen = false
                this.ws = null
                console.log('[failed] WebSocket connect failed')
                return
              }
              // 重連嘗試
              this.errorResetTimer = setTimeout(() => {
                this.init()
                this.reconnectNumber++
                console.log(`[socket reconnecting ${this.reconnectNumber} times...]`)
              }, this.reconnectNumber * 1000)
            }

            /**
             * 事件輪詢器
             * @param {function} event 事件
             * @param {number|string} outerConditon 停止條件
             * @param {number} time
             * @param {function} callback
             */

            static eventPoll(event, outerConditon, time, callback) {
              let timer
              let currentCondition
              timer = clearInterval(() => {
                if (currentCondition === outerConditon) {
                  clearInterval(timer)
                  callback && callback()
                }
                currentCondition = event()
              }, time)
            }

            /**
             * 初始化連接,開始訂閱消息
             * @param {function} callback
             */

            init(callback) {
              // 如果已經手動關閉socket,則不允許初始化
              if (this.closeSocket) {
                throw new Error('[Error] WebSocket has been closed.')
              }

              // 清除心跳檢測計時器
              this.heartbeatTimer && clearTimeout(this.heartbeatTimer)
              this.ws = new WebSocket(this.url)

              this.ws.onopen = () => {
                callback && callback()
                this.reconnectNumber = 0
                this.disconnectSource = ''
                this.errorResetTimer = null
                this.errorDispatchOpen = true
                // 訂閱消息
                this.subscribe()
                // 開啟心跳偵測
                this.heartbeatDetect()
                console.log('[Open] Connected')
              }

              this.ws.onclose = VueSocket.errorDispatch('close')
              this.ws.onerror = VueSocket.errorDispatch('error')
            }

            /**
             * 訂閱器
             */

            subscribe() {
              this.ws.onmessage = (res) => {
                if (res.data) {
                  const data = JSON.parse(res.data)
                  // 根據(jù)任務類型,分發(fā)數(shù)據(jù)
                  try {
                    this.distributeData && this.distributeData(data, this.commit)
                  } catch (e) {
                    console.log(e)
                  }
                }
                // 收到消息關閉上一個心跳定時器并啟動新的定時器
                this.heartbeatDetect()
              }
            }

            /**
             * 發(fā)布器(組件發(fā)消息的)
             * @param {String} data
             * @param {Function} callback
             */

            emit(data, callback) {
              const state = this.getSocketState()
              if (state === this.ws.OPEN) {
                this.ws.send(JSON.stringify(data))
                callback && callback()
                this.heartbeatDetect()
              } else if (state === this.ws.CONNECTING) {
                // 連接中輪詢
                VueSocket.eventPoll(state, this.ws.OPEN, 500, () => {
                  this.ws.send(JSON.stringify(data))
                  callback && callback()
                  this.heartbeatDetect()
                })
              } else {
                this.init(() => {
                  this.emit(data, callback)
                })
              }
            }

            /**
             * 心跳偵測
             */

            heartbeatDetect() {
              this.heartbeatTimer && clearTimeout(this.heartbeatTimer)
              this.heartbeatTimer = setTimeout(() => {
                const state = this.getSocketState()
                if (state === WebSocket.OPEN || state === WebSocket.CONNECTING) {
                  // 發(fā)送心跳
                  this.ws.send('ping')
                } else {
                  this.init()
                }
              }, 50000)
            }

            /**
             * 手動關閉連接
             */

            close() {
              this.heartbeatTimer && clearTimeout(this.heartbeatTimer)
              this.errorResetTimer && clearTimeout(this.errorResetTimer)
              this.closeSocket = true
              this.ws.close()
            }
            /**
             * 手動連接
             */

            open() {
              if (!this.closeSocket) {
                throw new Error('[Error] WebSocket is connected')
              }
              this.heartbeatTimer = null
              this.reconnectNumber = 0
              this.disconnectSource = 0
              this.errorResetTimer = null
              this.errorDispatchOpen = true
              this.closeSocket = false
              this.init()
            }

            /**
             * 獲取當前socket狀態(tài)
             */

            getSocketState() {
              return this.ws.readyState
            }
          }

          export default VueSocket

          復制代碼

          在Vuex中定義初始化WebSocket連接的actionmutation

          import { createStore, createLogger } from 'vuex'
          import VueSocket from '@/utils/VueSocket'
          import { round } from '@/use/useToolFunction'
          import {
            handleData
          from '@/utils/handleSocketData' // 分發(fā)任務的關鍵函數(shù)
          import { WEBSOCKET } from '@/config' // 導出一個常量

          const debug = import.meta.env.MODE.startsWith('dev')
          const store = createStore({
            state: {
              wsnull // websorket實例
            },
            mutations: {
              // 初始化socket連接
              createSocket(state, { commit }) {
                const baseURL = `${import.meta.env.VITE_APP_SOCKET}?portName=${
                  WEBSOCKET.TARGET
                }
          `

                state.ws = new VueSocket(baseURL, commit, handleData)
              },
            },
            actions: {
              // 創(chuàng)建實例
              socketInit({ commit }) {
                commit('createSocket', { commit })
              }
            }
            // debug console
            // plugins: debug ? [createLogger()] : [],
          })

          export default store

          復制代碼

          重點說一下這個handleData方法吧,VueSocket實例調用subscribe方法后就會訂閱服務器所有的消息,而這個方法就是根據(jù)消息里面的任務名把消息送達各個組件。

          比如現(xiàn)在有一個場景:有很多設備的子設備健康度需要實時展示:

          const handleData = (data, commit) => {
            // 當前任務
            const [task] = Object.keys(data)

            // 任務執(zhí)行器
            const taskRunner = {
              healthRunner() {
                const {
                  message: { dataContent }
                } = data[task]
                // 更新狀態(tài)
                dataContent &&
                  commit('updateHealthDegree', {
                    prop: task,
                    healthDegree: dataContent[0].health
                  })
              },
              defaultRunner(mutation) {
                const {
                  message: { dataContent }
                } = data[task]
                // 更新狀態(tài)
                dataContent && commit(mutation, dataContent)
              }
            }

            // 任務映射委托
            const taskMap = {
              // 健康度
              completeMachineHealthDegree() {
                taskRunner.healthRunner()
              },
              pressureHealthDegree() {
                taskRunner.healthRunner()
              },
              axletreeHealthDegree() {
                taskRunner.healthRunner()
              },
              gearboxHealthDegree() {
                taskRunner.healthRunner()
              }
            }

            // 執(zhí)行任務
            if (task in taskMap) {
              taskMap[task]()
            }
          }
          復制代碼

          這個方法十分關鍵,所有的任務其實只是一個對象中的屬性,然后映射的值是一個函數(shù),只要判斷這個任務在這個對象里面就會執(zhí)行對應的函數(shù)。而最后任務的執(zhí)行器其實就是調用了傳進來的commit函數(shù),觸發(fā)mutation變更狀態(tài)。我最開始是使用if/else或者switch/case來處理這個邏輯,但是隨著任務越來越多(20多個),代碼可讀性也變得糟糕起來,所以想了這個辦法處理。

          下面是Vuex的定義,store/getters必須與任務名對應:

          state: {
              completeMachineHealthDegree1// 整機健康度
              pressureHealthDegree1,        // 液壓系統(tǒng)健康度
              axletreeHealthDegree1,        // 泵健康度
              gearboxHealthDegree1          // 齒輪箱健康度
          },
          getters: {
              pressureHealthDegree(state) {
                return round(state.pressureHealthDegree, 2)
              },
              axletreeHealthDegree(state) {
                return round(state.axletreeHealthDegree, 2)
              },
              gearboxHealthDegree(state) {
                return round(state.gearboxHealthDegree, 2)
              },
              completeMachineHealthDegree(state) {
                return round(state.completeMachineHealthDegree, 2)
              }
          },
          mutation: {
              // 健康度
              updateHealthDegree(state, { healthDegree, prop }) {
                state[prop] = healthDegree
              }
          }
          復制代碼

          萬事俱備只欠東風,有了初始化連接的方法后,現(xiàn)在App.vue這個組件觸發(fā)一下action

          App.vue

          import { useStore } from 'vuex'

          const store = useStore()
          store.dispatch('socketInit')
          復制代碼

          連接建立后,就可以搞事情咯。我們先根據(jù)后端的數(shù)據(jù)格式封裝一個Compostion Function,因為有很多組件都需要使用這個實例發(fā)消息。

          src/use/useEmit.js

          import { useStore } from 'vuex'

          function useEmit(params{
            const store = useStore()
            const ws = store.state.ws
            const data = {
              msgContent`${JSON.stringify(params)}`,
              postsId1
            }
            ws.emit(data)
          }

          export default useEmit
          復制代碼

          在組件里面使用:

          HealthChart.vue

          import useEmit from '@/use/useEmit'

          // params由父組件傳進來,這里就不詳細展開了
          const params = { ... }

          onMounted(() => {
              useEmit(params)
          })
          復制代碼

          然后通過watchwatchEffect方法監(jiān)聽數(shù)據(jù)變化,每次變化都去調用echarts實例的setOption方法來重繪圖表,這樣就可以實現(xiàn)動態(tài)數(shù)據(jù)變更了,這里就不展開講了。

          2.4.5 數(shù)據(jù)Mock

          我們是前后端同步開發(fā),有時候會出現(xiàn)前端開發(fā)完接口沒開發(fā)完的情況,我們可以先根據(jù)接口文檔(沒有接口文檔可以問后端要數(shù)據(jù)庫的表)來Mock數(shù)據(jù)。我們通常有下面的解決方法:

          • 使用mockjs
          • 使用Node部署一個MockServer
          • 使用靜態(tài)JSON文件

          第一種我們比較常用,但是瀏覽器NetWork工具看不到發(fā)不出去的請求;第二種需要單獨寫一套Node服務,或者用第三方服務本地部署,很好用,但是有些麻煩;第三種比較原始就不考慮了。

          最后使用了vite-plugin-mock,這是一個基于Vite開發(fā)的插件。

          提供本地和生產模擬服務。vite 的數(shù)據(jù)模擬插件,是基于 vite.js 開發(fā)的。并同時支持本地環(huán)境和生產環(huán)境。Connect 服務中間件在本地使用,mockjs 在生產環(huán)境中使用。

          先安裝:

          yarn add mockjs
          yarn add vite-plugin-mock -D
          復制代碼

          配置:

          // vite.config.js

          import { viteMockServe } from 'vite-plugin-mock'

          export default defineConfig({
            plugins: [
              vue(),
              viteMockServe({
                supportTsfalse
              })
            ]
          })
          復制代碼

          它默認會在根目錄下請求mock文件下的數(shù)據(jù),不過可以進行配置。先這個文件下配置需要的數(shù)據(jù),然后在組件發(fā)請求就可以了。

          // mock/index.js

          export default [
            {
              url'/api/get',
              method'get',
              response({ query }) => {
                return {
                  code0,
                  data: {
                    name'vben'
                  }
                }
              }
            },
            {
              url'/api/post',
              method'post',
              timeout2000,
              response: {
                code0,
                data: {
                  name'vben'
                }
              }
            }
          ]
          復制代碼

          3.項目遇到的坑

          ViteVue3都是較新的技術,而且使用UI框架也是beta版本,遇到的坑真是不少,大部分的坑都是靠著官方的issue來解決的,好在都能找到對應的issue,不然得自己提issue等待回復了。

          3.1 啟動項目就報錯(esbuild error)

          后面查看相關issue,主要可以從下面幾個方法嘗試:

          • 不要用中文名路徑
          • 不用Git Bash來啟動
          • 刪除node_modules重新安裝
          • npm換成yarn

          具體issue忘記記錄了,大家如果也碰到相關問題可以按照上面的方式嘗試。

          3.2 @vue/compiler-sfc 3.0.7 版本后打包后,style scoped里面的樣式失效

          相關 issues:

          • Scoped CSS not generating correctly
          • Vue 3 scoped styles do not work on preview

          解決方案:將@vue/compiler-sfc鎖定為版本3.0.7

          3.3 父組件使用ref訪問子組件(子組件使用了 setup sugar)時,ref值為{}

          相關issue:

          • Setup sugar cannot pass the ref object to the ref function param.But I can get the ref object correctly with no sugar

          官方目前還在討論具體的解決方案,我們現(xiàn)在只需要在這種場景下讓子組件不使用setup sugar。

          4.可優(yōu)化的地方

          4.1 線上錯誤監(jiān)控(sentry)

          Vite生產環(huán)境下是通過rollup進行打包的,即使本地開發(fā)進行了測試也沒有復現(xiàn)的BUG,但是我們是無法知道用戶的使用場景的,線上的BUG總會有我們想不到的地方,這一塊的基礎設施后續(xù)有時間必須安排上。

          4.2 Monorepo

          我為了追求速度,搭建完一套系統(tǒng)后,就復制它給其他系統(tǒng)用。但其實里面有很多可以復用的模塊,除了上傳私有Npm,還有一個更好的方式就是Monorepo,它把所有子項目集中在一起管理,而且這幾個子項目都跟業(yè)務強相關,不用再切來切去了,最重要就的就是它只需要走一套CI/CD流程就行了。

          4.3 Git工作流

          Git工作流沒有搭建是因為我們就2-3個人,走這套流程時間不允許。但是以后有新成員加入,團隊人員變多后就需要這套工作流了。

          4.4 定制腳手架

          項目搭建完,可以把一些業(yè)務代碼與配置抽離出去,然后搭建一個自己的腳手架,當然也可以基于業(yè)務定制化腳手架,這樣以后有相關架構的項目可以直接基于這個腳手架開發(fā),節(jié)省前期基礎設施搭建的時間。

          4.5 多個Echarts組件實時渲染數(shù)據(jù)掉幀,吃內存

          這是我之前沒有考慮到的性能優(yōu)化問題,我以為我考慮很全面了,結果還是把最重要的性能優(yōu)化給忘了,這是我的失職。所以以后開發(fā)任何業(yè)務功能,都需要考慮性能,在滿足基本需求下,加大量級去考慮問題。

          比如我這個Echarts圖表渲染問題,20多個圖表,上萬的數(shù)據(jù)實時渲染,目前還只是掉幀吃內存,如果把量級加大到幾十萬條數(shù)據(jù)呢?

          5.總結

          本文主要是對我前三個月所做項目的總結與反省,我從項目搭建角度出發(fā),給大家講述了如何讓項目變得規(guī)范和嚴謹,最后得出一些自己的思考,我希望自己能從這次項目中成長起來,也希望給大家?guī)硪淮畏窒?,從中受益,也歡迎大家批評指正。

          最后還要提一嘴的是,我們的團隊很小,也不是大公司,正因為這個原因我才有機會嘗試這些新鮮技術,并用于實戰(zhàn),但是我們也需要承擔自己的責任,出了任何問題都要站出來解決。

          點贊和在看就是最大的支持??

          瀏覽 78
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  操BXXXXX | 日韩人妻无码精品一区 | 久久天天躁狠狠躁夜夜爽 | 国产人妻AV | 丁香无码视频 |