<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的項目復(fù)盤總結(jié)

          共 32525字,需瀏覽 66分鐘

           ·

          2021-07-02 03:08

          作者:不燒油的小火柴

          https://juejin.cn/post/6969758357288648718


          1.項目背景與技術(shù)選型

          1.1 項目背景

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

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

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

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

          1.2 技術(shù)選型

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

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

          image.png

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

          2.基礎(chǔ)設(shè)施搭建

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

          image.png

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

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

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

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

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

          2.1 缺陷管理

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

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

          2.1.1 代碼風(fēng)格約束

          我們先來看看在Vue3.0的項目中如何使用ESLintPrettier對代碼風(fēng)格進(jìn)行約束。

          首先安裝這幾個包:

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

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

          .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'
            }
          }
          復(fù)制代碼

          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'
          }
          復(fù)制代碼

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

          • ESLint Rules
          • Prettier中文網(wǎng)

          2.1.2 Git提交約束

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

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

          先配置適配器:

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

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

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

           "config": {
              "commitizen": {
                "path""cz-conventional-changelog"
              }
            }
          復(fù)制代碼

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

          "scripts": {
              "cz""git cz"
            }
          復(fù)制代碼

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

          image.png

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

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

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

          有了這個校驗工具,怎么才可以觸發(fā)校驗?zāi)兀覀兿M谔峤淮a的時候就進(jìn)行校驗,這時候husky就可以出場了,他可以觸發(fā)Git Hook來執(zhí)行相應(yīng)的腳本,而我們只需要把剛剛的校驗工具加入腳本就可以了,我們使用的是6.0的新版本,下面是具體使用方法:

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

          • 提交前對暫存區(qū)的文件進(jìn)行代碼風(fēng)格語法校驗
          • 對提交的信息進(jìn)行規(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"
              ]
            }
          復(fù)制代碼

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

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

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

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

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

          2.1.3 IDE環(huán)境約束

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

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

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

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

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

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

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

          現(xiàn)在有了制品倉庫就需要持續(xù)部署(CI),但是我技術(shù)能力不夠,只能借助Jenkins執(zhí)行腳本來實現(xiàn),但是原則上來說在Docker容器里面部署Docker容器這樣的做法并不好,很容易出現(xiàn)一些問題,我想學(xué)習(xí)完Kubernetes后再來對這個流程進(jìn)行優(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;"]
          復(fù)制代碼

          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應(yīng)用的history模式路由需要在前端配置500、400錯誤
          }
          復(fù)制代碼

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

          2.3 CSS樣式管理

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

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

          yarn add sass -D
          復(fù)制代碼

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

          <style lang="scss" scoped>
          // ...
          </style>
          復(fù)制代碼

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

          src/assets/styles:

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

          index.scss

          @import './variables.scss';
          @import './mixins.scss';
          @import './common.scss';
          @import './transition.scss';
          復(fù)制代碼

          然后在main.js導(dǎo)入index.scss就可以使用了:

          import '/src/assets/styles/index.scss'
          復(fù)制代碼

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

          image.png

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

          vite.config.js

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

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

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

          2.4 接口管理

          2.4.1 基于Axios二次封裝

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

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

          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')
          復(fù)制代碼

          在組件中使用:

          import { getCurrentInstance } from 'vue'

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

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

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

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

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

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

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

          .env.development

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

          .env.production

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

          全局變量使用方式:

          import.meta.env.VITE_APP_BASE_API
          復(fù)制代碼

          2.4.3 跨域問題

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

          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, '')
                }
              }
            }
          復(fù)制代碼

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

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

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

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

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

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

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

          下面是一個簡單的圖:

          image.png

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

          下面是一個完整的例子:

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

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

          src/utils/VueSocket.js

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

            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 // 開啟錯誤調(diào)度
              this.closeSocket = false // 是否關(guān)閉socket
              this.init()
            }

            /**
             * 錯誤調(diào)度
             * @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) {
              // 如果已經(jīng)手動關(guān)閉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ù)任務(wù)類型,分發(fā)數(shù)據(jù)
                  try {
                    this.distributeData && this.distributeData(data, this.commit)
                  } catch (e) {
                    console.log(e)
                  }
                }
                // 收到消息關(guān)閉上一個心跳定時器并啟動新的定時器
                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)
            }

            /**
             * 手動關(guān)閉連接
             */

            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()
            }

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

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

          export default VueSocket

          復(fù)制代碼

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

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

          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

          復(fù)制代碼

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

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

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

            // 任務(wù)執(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)
              }
            }

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

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

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

          下面是Vuex的定義,store/getters必須與任務(wù)名對應(yīng):

          state: {
              completeMachineHealthDegree1// 整機(jī)健康度
              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
              }
          }
          復(fù)制代碼

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

          App.vue

          import { useStore } from 'vuex'

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

          連接建立后,就可以搞事情咯。我們先根據(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
          復(fù)制代碼

          在組件里面使用:

          HealthChart.vue

          import useEmit from '@/use/useEmit'

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

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

          然后通過watchwatchEffect方法監(jiān)聽數(shù)據(jù)變化,每次變化都去調(diào)用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服務(wù),或者用第三方服務(wù)本地部署,很好用,但是有些麻煩;第三種比較原始就不考慮了。

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

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

          先安裝:

          yarn add mockjs
          yarn add vite-plugin-mock -D
          復(fù)制代碼

          配置:

          // vite.config.js

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

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

          它默認(rèn)會在根目錄下請求mock文件下的數(shù)據(jù),不過可以進(jìn)行配置。先這個文件下配置需要的數(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'
                }
              }
            }
          ]
          復(fù)制代碼

          3.項目遇到的坑

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

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

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

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

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

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

          相關(guān) 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值為{}

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

          4.2 Monorepo

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

          4.3 Git工作流

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

          4.4 定制腳手架

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

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

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

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

          5.總結(jié)

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

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


          最后

          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會很認(rèn)真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對你有幫助,在看」是最大的支持
           》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持
          瀏覽 81
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  五月婷婷久久综合 | 欧美黄色成人影片下载大全 | 午夜激情五月天 | 特黄AAAAAAA免费无码 | 18禁www. |