前一篇對後端接口開發做了一個簡簡單單介紹,不夠專業,這一篇主要對《樹洞》前端做一個介紹。對於裏面某些細節的部分不再做說明,比如移動端適配,可以參考我之前《樹洞》系列文章。
《樹洞》這次的需求設計並不像最初那麼複雜,主要在於技術實踐。在開始做這件事的時候,需求設計就像電影《解憂雜貨鋪》那樣,分爲兩個部分:一個部分是公開信箋,另一個部分是私有信箋。從功能上,它們只是可見性的區別而已,所以最後只做了公開信箋。
功能需求
和電影不同的是公開信箋並不只是由後臺管理員回覆,而是隨機從用戶中抽取回復人,且只有該用戶擁有回覆權限。所有人都可以看到信箋內容和回覆內容,在設計上是允許對信箋和回覆發表自己的態度、分享,在做的過程中只做了對原信箋的贊同。分享因爲分享設計第三方接口,所以未開發。官網的功能還包括用戶的登錄、註冊、修改密碼、修改個人資料、查看創建內容和查看回復內容。
後臺管理系統功能設計的功能包括對後臺權限的增改查、後臺用戶的增改查、官網用戶的增改查、信箋及用戶對信箋操作數據的查看。起初的設計中這些功能還包括刪除操作,對後臺用戶、官網用戶的啓用、禁用操作、信箋的審覈等等。不過,一個人的精力實在有限,而關鍵的一點是它不能成爲一個商業化的東西。於是不斷的簡化簡化,儘可能去做一些技術上的練習。畢竟平時上班也在不停地重複一些界面功能,沒什麼懸念可言,最終只留下了一些簡單的功能。
技術選型
最開始標題準備寫《Web項目實踐@樹洞(前端篇)》,轉念一想,搞得好像到終點站一樣。然而並不是這樣,《樹洞》項目在早使用的vue2.x,現在使用的vue3.x,之後還會有react、angular,於是加上一個vue3宣示主權。在開始寫這個項目的時候覺得vue3.0還是少了點挑戰,加了個TypeScript,還是感覺少點什麼於是又決定用vite。現在項目寫完了,先做個總結。
首先,雖然標榜使用TypeScript,但是最終邏輯並沒有完全執行TypeScript寫法。簡單地嘗試了一下,寫着寫着有些雞肋的感覺。比如有一個表單,我要先去聲明一個表單裏面的數據類型。再比如傳遞給接口的參數爲number和string的聯合類型,但是在傳參的時候它變成值就是其中一種類型,然而被認爲是null而報錯。最後實在受不了,我全換成了any類型。對於TypeScript的寫法,我再適應一段時間,可能就習慣了。
然後是vite,對於這個工具其實也沒有太多想說的,它的編譯效率不可否認要快不少,只是我總感覺它的模板並不如vue-cli成熟。比如別名,說實話我沒太明白它的配置規則,這個在後面的vite配置再做討論。有一點特別爽的是:在對包進行升級的時候不會像vue-cli受到webpack的約束。
最後,沒有最後。vue整體上是比較簡單的,哪怕是到了vue3,只要vue2比較熟悉,看看vue3遷移指南基本上沒啥大問題。只是vue3對應的生態圈,諸如組件、插件之類的不如vue2豐富。就拿UI庫來說,我最常用的是element和ant-design。ant-design-vue2.x看似已經轉入正式版了,其實它還在不停的變動,而且還有比較的變動。比如useForm在v2.2之前需要單獨引入,在這之後已經集成到了Form。而element-plus截止到我寫這篇文章,還處於beta版或者alpha版(一直在搖擺),這完全不敢用啊。
element和ant-design這兩個UI庫說不上誰好誰不好。ant-design在功能上稍微要更豐富一些,比如Form組件的useForm、Table組件的columns屬性、Input的pressEnter事件等等。而element在細節上要稍微更注重一些,比如InputNumber的加減按鈕、Select的異步搜索、Image的加載狀態等等。因此,這兩個庫談不上誰更優秀。element還處於不穩定狀態,於是管理後臺使用ant-design。原本打算element和ant-design在管理後臺和官網上各使用一個,後捨棄了做PC,也就捨棄了用element-plus。
開始折騰
npm init vite@latest
首先創建項目,截圖是我最開始搭建項目時所留下,關於vite的相關信息請參考Vite官方文檔。
切換目錄到項目下,開始安裝包,然後讓項目跑起來。哎,怎麼跑不起來,是因爲沒腿嗎?
沒腿就讓它長上腿吧。
node node_modules/esbuild/install
項目可以跑起來了,下面我們開始引入UI庫,做一些配置。先說一下配置,vue-cli的配置是在vue.config.js裏面配置,而vite是在vite.config.ts(因爲我是選的是ts模板)裏面。配置項大都與vue-cli差不多,當然也有不同的地方,比如插件,畢竟是不同的工具。再比如我們接着要說的UI庫ant-design-vue的主題定製,我們首先需要引入的不是css而是less文件,然後要開啓javascript,否則會報錯。
按照官網的配置方法配置,結果不生效。需要特別說明一下的是:我源碼中使用的less-loader是10.0,這也會有影響,不同版本的參數可能存在差異。不可否認,我最開始是直接安裝的less和less-loader,它們都是用的最新版,然後我去查官網資料,結果查到:
它告訴我這個配置項已經被廢棄了,是因爲這個原因嗎?當然,並不是,我立即按照官網的版本重新下載包,結果還是有問題。同樣的版本同樣的配方,還是不生效,爲什麼。再去查看vite、ant-design-vue、less-loader文檔,最後發現它一直在玩lessOptions這個配置項。去掉它,搞定,自定義終於生效了。在使用vue-cli的時候,我對sass-loader升級,結果一下子就報錯了,我就發現它和webpack有極強的關聯。
不僅如此,less也一樣,loader、nodejs、UI庫都有可能影響到它們,有時稍不注意一個升級就報錯了。如果我在vite升級,會出現什麼情況?帶着這個疑問,我用npm-check-updates對項目進行升級,結果發現沒什麼問題。於是我用同樣的配方對vant進行配置,等我配置完使用才發現,vant3.x根本不需要,因爲它使用的css4語法中的var進行變量的定義。
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true,
modifyVars: {
hack: `
true;
@import "${resolve(__dirname, 'src/styles/variables.less')}";
@import "${resolve(__dirname, 'src/styles/mixins.less')}";
`
}
}
}
}
到此爲止,我們的項目總該又能跑起來了吧?不,不行,因爲我們在配置less的javascriptEnabled的之前,有這麼一段代碼:
@import '~ant-design-vue/dist/antd.less';
這段代碼會導致程序報錯:
在vite裏面它不認識“~”了,不認識我們就需要去配置這個別名。既然它不認識“~”,同樣也不認識“@”。
然而,這兩個別名的配置讓我有些想不明白,先上代碼:
resolve: {
alias: [
{ find: '@', replacement: resolve(__dirname, 'src') },
{ find: /^~/, replacement: '' }
]
}
一個使用字符串,一個使用正則,必須這樣,否則不生效。“~”使用正則,我能想明白,它是在替換以“~”開頭的路徑。於是我就想用正則來匹配以“@/”開頭的路徑,結果不是我想要的結果。然後我去查看vue-cli的配置,沒發現什麼特別的地方,這讓我很納悶。這隻能留到後面再細細研究了,先把坑留在這兒。
現在UI庫引進來了,但是任務還沒有結束,vue-router、vuex還沒有,得自己引入。這樣的做法無疑在告訴開發者,你可以用你自己喜歡的解決方案。當然vue3幾乎可以用Provide / Inject取代vuex,不管怎麼樣,要用vuex就得自己引入。配置vue-router,我從vue-cli的配置中拷貝了一個過來,結果發現它報錯了:
process不存在?在官方文檔中createWebHashHistory並沒有傳這個參數。本着研究的原則,假如我需要這麼一個環境變量,怎麼辦?vite給出了自己的變量import.meta.env.BASE_URL。
OK路由已經加上,該讓代碼策馬崩騰了!
最後
還沒完呢,界面邏輯是沒問題了,我們還需要請求數據。我使用axios:一般情況下我們只會對正常的數據進行處理,其餘不管是請求和響應時的錯誤只需要做一個統一提示即可。除了響應攔截,我們還要對請求進行攔截,放入token之類的頭部參數,所以需要對axios進行二次封裝。另外,我們還需要對重複請求、路由跳轉的時候未完成的請求進行處理。
首先初始化創建一個axios:
const $axios = axios.create({
baseURL: '/api',
timeout: 10000
})
爲了處理重複請求、路由跳轉的時候未完成的請求進行處理,axios提供了cancel方法,我們需要將這些cancel存起來方便處理:
// cancel token存儲
const cancelToken = new Map()
/**
* 存儲cancel token
* @param config axios請求配置
*/
function setCancelToken(config: AxiosRequestConfig) {
const url = [
config.method,
config.url,
qs.stringify(config.params),
qs.stringify(config.data)
].join('&')
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
if (!cancelToken.has(url)) {
cancelToken.set(url, cancel)
}
})
}
/**
* 移除cancel token
* @param config axios請求配置
*/
function deleteCancelToken(config: AxiosRequestConfig) {
const url = [
config.method,
config.url,
qs.stringify(config.params),
qs.stringify(config.data)
].join('&')
if (cancelToken.has(url)) {
cancelToken.get(url)()
cancelToken.delete(url)
}
}
/**
* 清除所有cancel token
*/
export function clearCancelToken() {
for (const cancel of cancelToken.values()) {
cancel()
}
cancelToken.clear()
}
路由跳轉時需要清除所有未完成的請求,所以要將其暴露出去。最後配置路由攔截,因爲我要統一處理錯誤,所以我做了以下封裝:
/**
* 錯誤處理,401提示登錄,其餘提示錯誤信息
* @param error 錯誤數據
*/
function errorHandle(error: any) {
// 提示處理
}
/**
* 處理正常響應數據處理錯誤
* @param response axios響應數據
* @returns 錯誤信息
*/
function misdataHandle(response: AxiosResponse) {
const err = {
status: response.status,
statusText: response.statusText,
code: response.data && response.data.code,
message: response.data ? response.data.message : createError(response.status),
data: response.data && response.data.data
}
// 錯誤處理
errorHandle(err)
return err
}
/**
* 響應異常錯誤處理
* @param error axios異常錯誤數據
* @returns 錯誤信息
*/
function exceptionHandle(error: AxiosError) {
let err: any = {}
if (error.response) {
err = misdataHandle(error.response)
} else if (error.request) {
err = misdataHandle(error.request)
} else {
err.message = error.message || createError()
}
// 非cancel token產生的錯誤處理
if (!axios.isCancel(error)) {
deleteCancelToken(error.config)
errorHandle(err)
}
return Promise.reject(err)
}
其中createError()是當沒有拿到錯誤信息時,根據狀態碼返回固定的錯誤信息,完整的封裝請參考源碼。然後,在路由中引入clearCancelToken通過路由守衛調用清除未完成的請求,在接口文件引入暴露的axios實例統一管理接口。
具體的業務邏輯就不說了,沒什麼新意,打完收工!
## 代碼倉庫 ##
前後端所有代碼都在一個倉庫不同的分支,代碼拉下來切換分支即可。
##@樹洞系列文章##
Web項目實踐@樹洞(前端篇vue3)