大型前端應用如何做系統融合?
1. 背景介紹
1.1業務介紹
A平臺與B平臺同屬于同一系統鏈路上,前者主要致力于為用戶提供注冊入駐服務,后者則專注于提供具體業務操作服務。兩者皆為運營人員所依賴的在線管理工具。
1.2現狀分析
目前這兩個平臺服務于同一業務方,且B應用的頁面已經100%嵌入到了A應用的平臺上,除此以外目前存在系統上及體驗上的痛點如下:
所以當時我們考慮既然服務于同一業務方是否能在代碼層面上將兩個平臺進行融合,通過系統的融合來達到優化用戶體驗以及降本增效的效果呢?
2.成果展示
平臺融合后,主要的優化點體現在以下四方面:
優化前(跳轉單個頁面白屏時間達2998ms左右):
優化后(跳轉單個頁面白屏時間800ms左右):
3. 具體措施
3.1方案調研
3.1.1部署方式
?部署優化:A應用前后端合部署,現計劃分離前端獨立部署;
?資源節約:經行云部署平臺調研,擬采用混合部署策略,將A應用與B應用前端靜態資源集中部署于一組容器,以優化資源利用;
3.1.2代碼倉庫整合
?A應用的三個項目與后端共享一個代碼倉庫,采用統一的編碼標準;而B應用則使用獨立的代碼倉庫,需從中分離出前端代碼,并確保分離過程不影響現有配置;
3.1.3項目框架
?4個項目的技術選型框架都為vue2,依賴項略有不同;
3.1.3系統權限
?A應用和B應用為erp登錄;
3.2架構設計
為了讓用戶融合無體驗差別,兩個平臺的用戶繼續使用各自的域名進行訪問,融合后的項目可以自動識別當前環境,加載對應的內容;保證融合前后用戶查看的內容是一致的;
3.3具體方案
3.3.1 目錄結構設計
針對融合,我們首先考慮的是融合后如何防止文件沖突,減少融合的復雜度,降低出問題的概率。保證兩個系統能正常運行;拆分邏輯分以下三個方面:
1)文件拆分與分類
兩個系統涉及到幾十個文件,經過分析,我們將其拆分成以下幾部分內容:【頁面文件、公共組件文件、mock文件、AxPI接口文件、基礎請求封裝文件、路由組件文件、Store文件、公共樣式文件、公共方法組件、mainjs文件、index.html文件】
2)結構整合與差異化處理
由于兩個項目的結構相似,我們可以針對各個部分進行整合。整體的思路是,對于差異比較大的文件,建立兩個獨立的文件夾,分別包含系統A和系統B的內容;然后通過一個index文件,識別到當前的運行環境是系統A還是系統B,再分別加載對應的內容;
3)內容融合與沖突解決
針對差異比較小或者無差異的文件,我們將文件內容進行融合。對于沖突的內容,我們進行了手動修改,并對全局引用部分進行同步修改;
├── root
│ ├── mocks
│ ├── public
│ ├── src
│ │ ├── api
│ │ │ ├── apiA // 存儲 A 業務請求接口
│ │ │ ├── apiB // 存儲 B 業務請求接口
│ │ │ ├── apiC // 存儲 C 業務請求接口
│ │ │ ├── baseHttp.js // 封裝基礎請求
│ │ │ ├── ARequest.js // A業務的公共處理,請求header和響應code碼等處理
│ │ │ ├── BRequest.js // B業務的公共處理,請求header和響應code碼等處理
│ │ │ ├── CRequest.js // C業務的公共處理,請求header和響應code碼等處理
│ │ │ ├── DRequest.js // D業務的公共處理,請求header和響應code碼等處理
│ │ ├── assets
│ │ │ ├── icons // icon內容
│ │ ├── common
│ │ │ ├── config
│ │ │ ├── styles // 一些公共的樣式
│ │ ├── components // 公共組件
│ │ ├── directive // 自定義指令
│ │ ├── layout //公共布局
│ │ ├── router
│ │ │ ├── a.js // 對應a應用
│ │ │ ├── b.js // 對應b應用
│ │ │ ├── c.js // 對應c應用
│ │ │ ├── index.js
│ │ ├── store
│ │ │ ├── modules
│ │ │ ├── getters.js
│ │ │ ├── index.js
│ │ ├── utils
│ │ ├── views
│ │ │ ├── a // a 業務文件
│ │ │ ├── b // b 業務文件
│ │ │ ├── c // c 業務文件
│ │ ├── main.js
│ │ └── App.vue
│ ├── env
│ ├── package.json
3.3.2. 應用類型判斷
應用類型判斷是我們重要的一環,是我們識別環境的基礎,當用戶通過不同的域名訪問到應用的時候,前端維護一個映射表,不同的域名代表不同的應用;在main.js文件中會在第一時間執行判斷識別;
let APPLICATION_TYPE = 'a'
let host = window.location.host;
// 維護域名列表,包含測試和線上環境
const A_HOST = ['a.com','a_pre.com']
const B_HOST = []
const C_HOST = []
const D_HOST = []
if(A_HOST.includes(host)){
APPLICATION_TYPE = 'a'
}else if(B_HOST.includes(host)){
APPLICATION_TYPE = 'b'
}else if(C_HOST.includes(host)){
APPLICATION_TYPE = 'c'
}else if(D_HOST.includes(host)){
APPLICATION_TYPE = 'd'
}
// 掛載全局
window._APPLICATION_TYPE = APPLICATION_TYPE
3.3.3. 路由設計
根據不同的域名獲取路由配置,根據路由配置生成路由;系統A和系統B各自維護一個路由列表;當從后端請求回來路由結構之后,根據不同的應用映射不同的文件內容;其中路由path保持不變,compoent增加前綴(應用類別)找到對應的應用下的組件;
?第一步:前端獲取當前域名,確認當前應用
根據全局的 APPLICATION_TYPE 字段識別
?第二步:前端維護一個路由列表
let router=[
{
path: '/home',
component: Layout,
meta: { title: '首頁', icon: 'el-icon-s-grid', alwaysShow: true },
redirect: '/home',
children: [
{
path: '/home',
component: () => import('@/views/home/index'),
name: 'home',
meta: { title: '首頁', icon: ''}
}
]
}
]
?第三步:根據當前應用請求后端接口,獲取路由配置信息(component的路徑前拼接各個應用的文件名)
let RouterApi={'a':'/api1','b':'/api2','c':'api3'}
api.get(RouterApi[APPLICATION_TYPE])
component='各個應用文件名'+接口返回路徑
?第四步:針對在路由信息放置在前端的應用,前端維護一個路由的配置信息表
import dRouter from './d.json'
if(APPLICATION_TYPE==='d'){
router=dRouter
}
?第五步:根據路由配置信息,生成路由結構
?第六步:實例化Vue對象
3.3.4. 環境變量設計
環境主要分為以下幾種:mock環境、本地開發環境、測試環境、線上環境
不同的環境對應不同的變量文件,在變量文件中設置每個端需要用到的參數,結合 APPLICATION_TYPE 和變量文件的配置,獲取到對應的參數
示例:
# .env.pruduction
# a 業務
VUE_APP_A_BASEURL = ''
# b 業務
VUE_APP_B_BASEURL = ''
# c 業務
VUE_APP_C_BASEURL = ''
# d業務
VUE_APP_D_BASEURL = ''
3.3.5. 請求設計
1)公共請求的封裝
封裝基礎的公共請求createHttp.js,各業務基于公共的請求和各自的code碼,請求參數等信息進行再次封裝,然后可以按照業務需求調用;
?基礎請求:createHttp.js
?業務公共封裝:
a. ARequest.js(A業務公共參數和code碼處理)
b. BRequest.js (B業務公共參數和code碼處理)
c. CRequest.js(C業務公共參數和code碼處理)
d. DRequest.js(D業務公共參數和code碼處理)
?業務層調用:
a. api/apiA A業務接口
b. api/apiB B業務接口
c. api/apiC C業務接口
// 公共請求封裝 baseHttp.js
export const createHttp = (baseUrl, successFun = () => {}, errorFun = () => {}, requestInterceptor = () => {}) => {
const http = axios.create({
baseURL: baseUrl,
timeout: 5 * 60 * 1000,
withCredentials: true
})
// http request 攔截器
http.interceptors.request.use(
async config => {
await requestInterceptor(config)
return config
},
err => {
return Promise.reject(err)
}
)
// http response 攔截器
http.interceptors.response.use(successFun, errorFun)
return http
}
//A業務基礎請求
function requestInterceptor(){
// A系統公共請求參數處理...
}
function successFn(){
// A系統公共響應結果處理 統一新增
}
function errorFn(){
// A共異常處理 包括code碼等情況
}
export default createHttp(baseUrl,successFn,errorFn,requestinterceptor)
3)業務接口使用,根據不同的業務劃分不同的目錄分支
// A業務請求調用
ARequest.get(url,{params:data})
//B業務請求調用
BRequest.post(url,{params:data})
3.3.6. 權限和登錄
根據應用類型字段APPLICATION_TYPE,識別不同的環境,請求不同的服務端接口;不同的服務端代表了不同的應用;
針對不同的應用的未登錄情況,前端維護多套登錄處理邏輯,根據應用類型進行不同的處理邏輯;
3.3.7. 公共函數設計
創建一個公共的utils文件夾,針對兩個項目中的公共函數進行合并,針對有沖突的函數,進行命名修改,全局引入的部分進行路徑和函數的同步修改;
3.3.8. 腳手架配置設計
梳理了兩個項目的腳手架配置差異項及各個配置的作用,對配置作了部分的修改和優化,在滿足原有的功能情況下不影響正常的項目運行;
3.3.9. Vuex store設計
store的整體結構保持不變,在項目引用的地址也保持不變,由于項目中使用store的地方較多,保持結構不變能保證改動成本最小,針對兩個項目中模塊名重復的情況,手動把模塊里的內容進行合并;
針對現有的名稱重復內容不一樣的函數,根據應用類型字段 APPLICATION_TYPE 把兩個函數進行融合進行分別處理;
3.3.10. 頁面引用設計
?引用方式變更
由于業務需求,應用A中嵌套了應用B的頁面,之前通過iframe引用。融合后,頁面文件和組件不再隔離,可以直接引入應用B的文件和組件;
?后端數據打通
應用A的后端服務器上有一些功能,如下載列表,應用B項目需要使用時因數據不通難以直接引用。前端融合后,可以在應用B中直接引用應用A的頁面組件,實現業務的順暢使用。效果如下:
4. 總結
在經歷了為期兩個月的緊張工作后,我們成功地將兩個大型項目進行了深度整合,取得了顯著的階段性成果。通過這一融合過程,我們不僅統一了項目的代碼規范和架構,還顯著提升了組件的復用率。盡管在這個過程中我們遇到了諸多挑戰和曲折,但最終的成果——用戶體驗的顯著提升——使一切努力都顯得彌足珍貴。
我們深知,每一個成功的項目背后都有無數次的嘗試和優化。在這個過程中,我們不斷學習、適應和完善,最終實現了項目的無縫融合。我們相信,這段經歷和我們所取得的成果,不僅為我們團隊帶來了寶貴的經驗和教訓,也可能為那些正在經歷類似挑戰的同學提供了一些啟示和幫助。