基于Vite2+Vue3的項目復(fù)盤總結(jié)
作者:不燒油的小火柴
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,甚是刺激!我簡單畫了一張圖:

接下來對項目涉及到的知識進(jìn)行梳理與總結(jié),我希望從這個“增刪改查”項目學(xué)到一些不一樣的知識,如果對您也有用,那就更好啦??。
2.基礎(chǔ)設(shè)施搭建
在講述前,先把我畫的線框圖呈上,方便對整個項目的基礎(chǔ)設(shè)施有個大概的了解:

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+PrettierGit Message提交規(guī)范:Angular提交規(guī)范
2.1.1 代碼風(fēng)格約束
我們先來看看在Vue3.0的項目中如何使用ESLint和Prettier對代碼風(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.js和prettier.config.js(拓展名可以自由選擇),我們把下面內(nèi)容加進(jìn)去:
.eslintrc.js
module.exports = {
parser: 'vue-eslint-parser',
env: {
browser: true,
node: true,
es2021: true
},
extends: ['plugin:vue/vue3-recommended', 'plugin:prettier/recommended'],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module'
},
rules: {
'prettier/prettier': 'error'
}
}
復(fù)制代碼
prettier.config.js
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: false, // 未尾逗號
vueIndentScriptAndStyle: true,
singleQuote: true, // 單引號
quoteProps: 'as-needed',
bracketSpacing: true,
trailingComma: 'none', // 未尾逗號
arrowParens: 'always',
insertPragma: false,
requirePragma: false,
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ī)范:

此時我們已經(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私有倉庫

現(xiàn)在有了制品倉庫就需要持續(xù)部署(CI),但是我技術(shù)能力不夠,只能借助Jenkins執(zhí)行腳本來實現(xiàn),但是原則上來說在Docker容器里面部署Docker容器這樣的做法并不好,很容易出現(xiàn)一些問題,我想學(xué)習(xí)完Kubernetes后再來對這個流程進(jìn)行優(yōu)化。
搭建完流程后,我們只需要在項目中寫好Dockerfile和Nginx的配置文件就可以了,下面是我項目中的一個案例:
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全部失效了,而且控制臺也報錯:

如果不使用這個變量,我在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: {
open: true,
proxy: {
'/api/v1/chart': {
target: 'http://192.168.12.116:8887',
changeOrigin: true
},
'/api/v1': {
target: 'http://192.168.12.116:8888',
changeOrigin: true,
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ā)的功能
下面是一個簡單的圖:

組件通過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)注emit和subscribe兩個方法,當(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連接的action和mutation:
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: {
ws: null // 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: {
completeMachineHealthDegree: 1, // 整機(jī)健康度
pressureHealthDegree: 1, // 液壓系統(tǒng)健康度
axletreeHealthDegree: 1, // 泵健康度
gearboxHealthDegree: 1 // 齒輪箱健康度
},
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)}`,
postsId: 1
}
ws.emit(data)
}
export default useEmit
復(fù)制代碼
在組件里面使用:
HealthChart.vue
import useEmit from '@/use/useEmit'
// params由父組件傳進(jìn)來,這里就不詳細(xì)展開了
const params = { ... }
onMounted(() => {
useEmit(params)
})
復(fù)制代碼
然后通過watch或watchEffect方法監(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({
supportTs: false
})
]
})
復(fù)制代碼
它默認(rèn)會在根目錄下請求mock文件下的數(shù)據(jù),不過可以進(jìn)行配置。先這個文件下配置需要的數(shù)據(jù),然后在組件發(fā)請求就可以了。
// mock/index.js
export default [
{
url: '/api/get',
method: 'get',
response: ({ query }) => {
return {
code: 0,
data: {
name: 'vben'
}
}
}
},
{
url: '/api/post',
method: 'post',
timeout: 2000,
response: {
code: 0,
data: {
name: 'vben'
}
}
}
]
復(fù)制代碼
3.項目遇到的坑
Vite和Vue3都是較新的技術(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é)任,出了任何問題都要站出來解決。
