審核平臺接入50+業務,提供在線審核及離線質檢、新人培訓等核心能力,同時提供數據報表、資源追蹤、知識庫等工具。隨著平臺的飛速發展,越來越多的新業務正在或即將接入審核平臺,日均頁面瀏覽量為百萬級別。如今審核平臺已是公司內容生產鏈路上的關鍵一環,是保障內容安全的重要防線,因此穩定性至關重要。
過去一年我們曾對前端項目進行框架升級,考慮風險與成本最小化,選擇了漸進式升級,利用微前端實現Vue2和Vue3共存,新接業務在Vue3倉庫中開發。經過一年的迭代,Vue3項目趨于穩定,沉淀了大部分通用能力。為了降低多倉庫維護心智,同時解決核心模塊的技術債務,考慮將剩余活躍代碼進行重構并遷移至新倉庫。
參考前端埋點報表,選擇老倉庫中頁面維度訪問量最高的路由,對線上使用情況進行摸排。日常業務現狀是點直融合,直播業務配置化接入需求較多,因為業務形態的差異,定制需求多,現有配置能力無法滿足,需擴充。開發現狀是通用配置化代碼改動頻繁,邏輯復雜,開發門檻較高,影響范圍大,牽一發而動全身。因此選擇配置化詳情頁作為優先重構并遷移的對象。
配置化詳情頁采用的是業務定制化的低代碼方案,包含schema渲染器和任務流兩部分。當前已沉淀近百份json schema,托管在內部其他低代碼平臺上。頁面覆蓋40多個業務,占據平臺約20%訪問量和35%獨立訪客。
圖片
圖片
如果將頁面看做一個黑盒子,依據唯一標識(路由path和query等)從node服務、平臺服務以及外部業務方服務獲取數據,基于頁面內部規則渲染頁面。審核員瀏覽并進行通過、駁回等操作,提交后將對視頻、彈幕等業務資源產生影響。
圖片
schema渲染器基于json schema和接口數據,在平臺內生成路由信息與頁面內容,負責各種模式的頁面分發、物料分發,并提供敏感詞、快照、洗數等通用平臺能力。
代碼現狀是數據獲取、提交操作和頁面復雜邏輯分散在vue文件和store中,業務邏輯和UI框架耦合嚴重,不利于集成自動化測試和框架升級。待辦、任務、資源等邊界劃分不清晰,平鋪在“巨石store“中,維護成本極高且代碼改動風險大。渲染器和任務流邏輯不夠內聚,耦合嚴重,無法做到關注點分離。因此需要尋找一種合適的架構進行重構,減弱業務邏輯對UI框架的依賴,增強可測試性。
整潔架構由Robert C. Martin在2012年提出,核心思想是將軟件系統拆分為獨立的層次,以實現高內聚、低耦合、可測試和可維護。
圖片
一共分為四個層級,環與環之間,存在一個依賴關系原則:源代碼中的依賴關系,必須只指向同心圓的內層,即由低層機制指向高級策略。
優點是可以在沒有UI、數據庫、web服務器或其他外部基礎設施的情況下測試業務邏輯;降低對UI框架的依賴,比如跨端開發時,業務邏輯可以復用,只需要做UI層的適配。相應的,缺點也很明顯,過于復雜,數據需要經過多層處理。學習成本較高,容易過度設計,增加復雜性,靈活性較低。適用于大型復雜項目,對于需要長期維護和持續開發的項目,清晰層次結構和明確依賴關系有助于減少代碼腐化,更容易適應需求變化。針對我們選擇的模塊,審核前端配置化頁面,比較適合。
圖片
圖片
圖片
主要可拆分成待辦、任務、資源等實體。待辦實體,主要提供待辦的基礎信息、獲取配置等屬性和方法。任務實體提供任務的狀態、任務耗時、調度配置、任務數據,計時和拉取任務流程等。資源實體提供資源的詳情數據、獲取詳情及數據清洗方法等。待辦實體包含了配置詳情頁所需的核心數據,任務實體高度抽象了核心任務流。在新業務接入過程中,實體層一般不變動,通過依賴倒置劃分架構邊界。
// entities/todo.jsexport default class Todo { todoId businessId todoConfig ... constructor() {} async getTodoConfig() { // 獲取配置 }}// entities/task.jsexport default class Task { dispatch_conf listData timeCount ... constructor() {} async getTaskDetail({ getTask, taskFormat, afterGetTask}) { // 抽象封裝核心任務流程 } // 計時邏輯 startTimer() {} clearTimers() {}}// entities/resource.jsexport default class Resource { detail dataReady constructor() {} async getResourceDetail({ getResource, resourceFormat, afterGetResource }) { // 抽象封裝資源模式核心流程 }}
用例層針對洗數、提交等復雜場景,通過調用實體層來實現特定的業務邏輯,是適配器層與實體層的中介。例如封裝了基于配置的洗數中間件,列表模式的單個和批量提交,卡片模式的單個和批量提交,快照上報、自動化質檢的復雜邏輯。用例層包含了系統的復雜業務邏輯,為單元測試提供了便利,可以獨立于UI和外部系統進行測試。
// usecase/use-single-submitimport { get } from 'lodash-es' // 三方工具庫import { ANNOTATION_SINGLE_OPER_PASS_NAME } from '@/constants' // 常量import { workbenchApi } from '@/api'import { setLogData } from '@/utils/xx' // 工具函數export function getSingleSubmitParams({ data, state.xxx }) { //... 邏輯處理 return params}export function submitAuditSingle({ data, afterTaskSubmit }) { const params = ... workbenchApi.submit(params).then((res) => { if(res.code = xxx){ afterTaskSubmit() // 調用鉤子函數 } })}
適配器層包含store和UI,調用用例層的代碼,主要負責將依賴UI、外部服務、設備等的數據處理為用例層可以使用的“干凈數據”。適配器層一般不包括復雜的業務邏輯,因此在框架遷移時僅需關注基本的框架差異,適合自動化代碼轉換。
// store/todoConfigDetailimport { getTodoInfo } from '@/struct/TodoConfigDetailStruct/usecase/use-todo'import { getTaskInfo, getTask, taskDispatchListFormat } from '@/struct/TodoConfigDetailStruct/usecase/use-task'import { getSingleSubmitParams, submitAuditSingle } from '@/struct/TodoConfigDetailStruct/usecase/use-single-submit'const todo = ref({})async function init({ $route }) { const query = $route.query const todoId = +query.todo_id ... set(todo, getTodoInfo({ todoId, ... })) await get(todo).getTodoConfig() ...}function getTaskDetail() { get(task).getTaskDetail({ getTask: async ({ noSeize, drillTaskIds }) => await getTask({ todo: get(todo), noSeize, drillTaskIds }), taskFormat: async (data) => await taskDispatchListFormat({ data, schema: get(todo).schema }) afterGetTask: (res) => { ... } })}async function submit(data) { if (single) { const params = getSingleSubmitParams({ data, todo: get(todo) }) submitAuditSingle({ params, afterTaskSubmit: () => { ... } }) } else { ... }}
// Audit.vueconst todoConfigDetailStore = useTodoConfigDetailStore()const { todo, task, multipleSelection } = storeToRefs(todoConfigDetailStore)const { getTaskDetail, submit } = todoConfigDetailStore
新業務接入一般對實體層和用例層無改動,僅需適配器層增加相應的展示物料,盡可能避免“牽一發動全身”。新架構會有一定的初學成本,但結合審核平臺復雜的項目現狀和持續接入新業務的節奏,長遠來看對于系統穩定性、可測試性有一定幫助,同時降低UI框架依賴性。
基于新的架構,可分層進行自動化測試和自動代碼轉換。
用例層為純函數,不依賴框架、設備、三方服務等。單元測試的技術棧為jest和vue-test-utils,從審核員的基本工作模式入手,針對任務領取、數據清洗與展示、稿件處理三個環節完善測試用例。因為是新老倉庫遷移,所以可以將線上環境視為基準進行用例采集。根據業務重要性、線上訪問情況,按優先級執行測試。單測能有效降低回歸成本,在新業務持續接入的背景下,保障系統穩定性。
在尋求升級方案的過程中,我們對比了兩款工具:Gogocode 和 vue2-to-composition-api。以下是它們的簡要對比:
功能 | Gogocode | vue2-to-composition-api |
缺點 | 默認不支持轉換成 Vue3 setup 語法 | 不支持 template 轉換 |
轉換規則覆蓋 | 轉換規則列表 | 轉換效果 |
特性 | 像 jQuery 一樣修改AST | 將 Vue2 代碼轉換為 Vue 3 的 Composition API 格式 |
2. jQuery-like API 簡化了 AST 修改成本 | 2. 支持在線轉換 | |
優點 | 1. 支持自定義插件 | 1. 支持轉換成 Vue3 setup 語法 |
經過調研,gogocode 是基于 AST 封裝的庫可擴展空間大,但是默認 gogocode-plugin-vue 不支持轉換成 Composition API
vue2-to-composition-api 倒是支持 Composition API,但是不支持 template 部分的轉換。
考慮到我們的項目中有許多自定義的轉換邏輯,如 UI 庫替換、store 替換等,我們最終決定使用 gogocode 作為主要工具,并結合其他手段來實現 vue2 到 vue3 的全面升級。
升級語法是借助 gogocode 的 replace 方法實現的,通過 $$$ 匹配符保留所需要的代碼塊,將 Vue2 的語法快速替換成 Vue3 的語法。
// 替換datascriptAst.replace("data() {return {$$$};}", `const $data = reactive({$$$})`);
// 替換propsscriptAst.replace("props:{$$$}", "const props = defineProps({$$$})");
// 替換生命周期scriptAst.replace("created(){$$$}", "onBeforeMount(()=>{$$$})") .replace("mounted(){$$$}", "onMounted(()=>{$$$})") .replace("async mounted(){$$$}", "onMounted(async ()=>{$$$})") .replace("beforeUnmount(){$$$}", "onBeforeUnmount(()=>{$$$})") .replace("unmounted(){$$$}", "onUnmounted(()=>{$$$})") .replace("beforeDestroy(){$$$}", "onBeforeUnmount(()=>{$$$})") .replace("destoryed(){$$$}", "onUnmounted(()=>{$$$})");
效果如下,通過 replace 替換的方式適合大部分場景,比如 methods、filters、watch 等
圖片
進階:處理模板中的變量、函數中的this變量
template綁定的變量可能是data,可能是props,還可能是 methods
想要替換 template 中的變量,需要先收集 data、props、methods 中的 keys
getDataKeys() { const keys = new Set(); // 只需要第一層的key,所以deep設為1 this.scriptAst.find('data() {$$$}').find('$_$:$_$', { deep: 1 }).each(node => { if (node.match[0] && node.match[0][0].node.type === 'Identifier') { keys.add(node.match[0][0].value); } }); return Array.from(keys)}getPropsKeys() { const keys = new Set(); this.scriptAst.find('props: {$$$1}', { deep: 1 }).each((node) => { if (node.match['$$$1']) { node.match['$$$1'].forEach((item) => { if (item.key && item.key.type === 'Identifier') { keys.add(item.key.name) } }) } }); return Array.from(keys)}// methods 有點小復雜,需要考慮異步函數,普通函數和鍵值對的寫法getMethodsKeys() { const methodsAst = this.scriptAst.find('methods:{$$$}'); const methods = methodsAst.find('$_$() {$$$1}'); const asyncmMethods = methodsAst.find('async $_$(){$$$1}'); const mapKeys = methodsAst.find('$_$:$$$1', { deep: 1 }); const methodNames = []; methods.each(node => { if (node.match[0] && node.match[0][0]) { methodNames.push(node.match[0][0].value); } }); asyncmMethods.each(node => { if (node.match[0] && node.match[0][0]) { methodNames.push(node.match[0][0].value); } }); mapKeys.each(node => { if (node.match[0] && node.match[0][0]) { methodNames.push(node.match[0][0].value); } }); return methodNames;}
收集完成后可以開始遍歷 template 中的 attr,并替換所綁定的變量了。
handlTemplate() { // 替換attr, 例如 <div :value="value"></div> this.ast.find("<template></template>").find(`<$_$ ="$$$0" >$$$1</$_$>`).each((node) => { node.match['$$$0'].forEach(attr => { if (attr && attr.value) { this.dataKeys.some(keyName => { const reg = new RegExp(`${keyName}//b`, 'g') const macth = reg.test(attr.value.content); attr.value.content = attr.value.content.replace(reg, `$data.${keyName}`) if (macth) { return true; } }) this.methodsKeys.some(keyName => { const reg = new RegExp(`//b${keyName}//b`, 'g') const macth = reg.test(attr.value.content); attr.value.content = attr.value.content.replace(reg, `methods.${keyName}`) if (macth) { return true; } }) this.propsKeys.some(keyName => { const reg = new RegExp(`//b${keyName}//b`, 'g') const macth = reg.test(attr.value.content); attr.value.content = attr.value.content.replace(reg, `props.${keyName}`) if (macth) { return true; } }) } }) // 替換content,例如:<div>{{value}}<div> node.match['$$$1'].forEach(node => { if (node.content && node.content.value) { // 省略:與上面類似 } }) }) }
說到 this 替換也是一個繁瑣的問題,this.xx, xx 可以是 data,可以是props,可以是 function,還可以是私有屬性等。
所以我們需要先把組件中的 data、props、methods、mapGetter 中的 keys 都收集一遍,然后再替換 script 中的 this 變量。
// 正則替換更方便,所以需要放在最后一步替換handlThis(code) { code = code.replace(/this/.([_$0-9a-zA-Z]+)/g, (match, $1) => { // 替換function body 中的 data引用 if (this.dataKeys.includes($1)) { return `$data.${$1}`; } // 替換function body 中的 methods調用 else if (this.methodsKeys.includes($1)) { return `methods.${$1}`; } // 替換 vm 私有屬性 else if ($1 && $1[0] === '$') { return `$vm.${$1}`; } else if (this.computedKeys.includes($1)) { return $1 } else if (this.propsKeys.includes($1)) { return `props.${$1}` } return `$vm.${$1}` }) // 替換function body 中的 動態methods調用 code = code.replace(/this/[(.+)/]/g, (match, $1) => { return `methods[${$1}]`; }) return code}
async parseOpenDialog(payload) { const { schema, data } = payload await this[schema.method]( parseSchema, data, dialogParams, dialogType )}
按 vue3 新的寫法,一般是展開的
const parseOpenDialog = async (payload) => {}
但是按這個習慣來轉換,就無法做到動態調用this.xxx,我們可以嘗試把方法都放在methods對象中,有點類似 vue2
const methods = { parseOpenDialog: async (payload) => { }, xxx: async() => { }}
在替換 this 時,將 this 替換成 methods 變成 methods[schema.method]()。
1 . 原來 vue2 中肯很多屬性掛在組件實例上,比如 $route, $router, $emit 甚至自定義的屬性等等。下面是 vue3 中獲取組件實例的方法。
import { getCurrentInstance } from 'vue'const { proxy: $vm } = getCurrentInstance()$vm.xxx = '自定義屬性'
2 . 原來使用的 vuex,現在使用的是 pinia。我們需要先收集 ...mapState($_$, [$$$1]),然后使用 storeToRefs 代替
// 獲取store 使用storeToRefsif (this.storeType === 'pinia') { this.scriptAst .find("computed:{}") .before(`const {${stateNames.join(',')}} = storeToRefs(${this.getPiniaStoreName(key)})`) .replace("computed:{$$$}", "$$$")}
最終將上述轉換能力封裝成一個庫,通過 npm 安裝來實現組件批量升級。
遷移前后的E2E測試 - 視覺輔助UI自動化測試
端到端測試是確保新舊系統平穩過渡的關鍵步驟。本次遷移依舊遵循漸進式升級的原則,新增v3路由,線上新老路由共存。共分為功能測試、UI測試、性能測試三個部分。功能測試為單元測試的補充,主要驗證新老路由下核心操作路徑及提交參數的一致性,攔截請求避免對線上造成影響。性能由通用監控大盤進行保障。UI對比測試是本次的重點。
新老倉庫分別基于Element UI和Element Plus,Element Plus重新設計了組件以適應Vue3,組件尺寸體系調整為更自然的大中小選項。間距優化為更通用的4px體系,主要涉及 padding 和 margin 屬性修改、 font-size 等字體和圖標大小修改等。因此,雖然大部分組件在外觀上保持相似,視覺和布局上可能有一些差異。由于業務組件具備一定復雜性,手寫測試用例工作量繁瑣,新舊頁面組件可能存在差異無法完全復用,方案也不具備通用性。因此考慮使用計算機視覺技術來識別和驗證用戶界面的元素。
基于公司內部的自動化測試平臺,測試框架為Playwright,測試語言選擇Python以更好的利用豐富的圖像處理庫。指定CSS選擇器,隨機選擇頁面上的元素進行截圖和對比,設定閾值進行判斷。圖像相似度對比可分為傳統的基于像素差的方法和基于圖像特征的方法。圖像特征有SIFT、ORB等特征提取方法和深度學習方法。分別選擇一種代表性的算法進行對比和測試。
v2 | v3 | SSIM | SIFT | LPIPS |
0.656 | 0.855 | 0.909 | ||
0.916 | 0.874 | 0.961 | ||
0.658 | 0.857 | 0.881 | ||
0.877 | 0.855 | 0.926 | ||
0.551 | 0.836 | 0.947 | ||
0.929 | 0.884 | 0.942 |
實驗表明,基于深度學習特征的相似度對比結果更接近用戶感知,針對Vue2升級Vue3 UI組件庫導致的間距、字體、尺寸等細微差異判斷更準確。因此采用LPIPS作為對比算法。
def compare_images(url1, url2): loss_fn = lpips.LPIPS(net = 'alex') img1 = cv2.imread(url1) img2 = cv2.imread(url2) if img1 is not None and img2 is not None and img1.size > 0 and img2.size > 0: img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0])) cv2.imwrite(url2, img2) combined_image = cv2.hconcat([img1, img2]) ex_img1 = lpips.im2tensor(lpips.load_image(url1)) ex_img2 = lpips.im2tensor(lpips.load_image(url2)) d = loss_fn.forward(ex_img1, ex_img2) if d is not None: cv2.putText(combined_image,'score: %.3f'%(1 - d.mean()), (20, 20), cv2.FONT_ITALIC, 0.4, (255, 0, 255)) return d, combined_image else: return None
依據業務流程,分別訪問新老路由,對頁面指定元素進行截圖、對比和拼接,輸出測試截圖和完整的測試報告。
圖片
圖片
采用視覺輔助UI自動化測試,更接近用戶真實感知,大大降低測試用例復雜度,提升測試效率,無需關注繁雜的DOM元素層級。
以頁面配置的形式按待辦進行灰度,跳轉至新路由。單元測試已集成至項目流水線中,MR和發布前觸發。灰度過程中手動執行E2E用例,以自定義環境變量的形式指定頁面路徑、元素選擇器、相似度閾值等。測試通過后修改頁面配置,引流至新路由。通過用戶故障群和頁面反饋入口響應用戶反饋,結合前端埋點報表觀察線上使用情況和確定灰度策略。通過完善單元測試、E2E測試和制定合理的灰度策略,針對特定模板的遷移已順利完成,期間未收到線上故障反饋。
后續遷移策略將以老倉庫中改動較頻繁文件優先入手,測試用例先行,借助自動代碼轉換工具快速平穩遷移,線上埋點數據做輔助。
活躍代碼陸續遷移,結束多倉庫并行,減少維護心智。之前我們的常態是新老倉庫并行,開發一個完整的業務功能時要在ts和js,選項式API和setup語法之間頻繁切換,心智負擔較重。活躍代碼陸續遷移至vue3新倉庫,結合新的框架特性和實用工具,能夠更專注于業務邏輯本身。
圖片
核心模塊重構,“巨石store”輕量化,提高可維護性和可演進性,分層結構保障核心邏輯穩定性。原本配置能力擴充時需要在復雜的數據流中“走迷宮”,耦合嚴重,通用代碼影響范圍大,常常出現A業務需求上線導致B業務不可用的情況。利用整潔架構進行分層設計后,新增一種審核模式僅需在適配器層新增對應物料和action,用例層新增用例。無需修改實體層和其他業務相關的用例、通用頁面、物料等。
圖片
圖片
業務邏輯的重新梳理,彌補測試用例空缺。領域提取是對業務邏輯的重新梳理,前端能加深業務理解。穩定性至上的模塊很長時間缺少測試用例,造成對開發人員的經驗、能力依賴極大。完善核心鏈路的測試用例能有效降低回歸成本,保障系統穩定性。
工具沉淀,組內復用。自動代碼轉換工具和基于AI能力的前端E2E測試方案為后續組內其他項目的框架升級和遷移提供了便利。
在2023年開始漸進式升級Vue3后,我們經歷了很長一段時間的多倉庫并行。在新業務不斷接入、開發新成員加入的背景下,這樣的模式無疑提升了開發門檻和維護心智。本次活躍代碼的陸續遷移結束了多倉庫并行的現狀,同時在整個實踐過程中我們為審核平臺這個大型復雜項目前端引入了整潔架構的思想,為后續的開發維護提供了一種新的思路。沉淀了一套自動化vue2代碼轉vue3 setup的工具,可為后臺項目的框架升級提供便利。同時借助AI能力提升前端E2E測試的效率,利用計算機視覺輔助前端UI自動化測試。有幾點心得:
完美重構、敏捷重構、系統穩定性難以平衡。
圖片
既然下定決心對年久失修的代碼進行重構,我們一定是追求極致優化和整潔的。但是需求現狀是不斷有新特性進來,戰線拉的太長必將導致抹平差異的成本增加,因此敏捷性也很重要。同時底線是關注系統的可靠性和穩定性。這三者一定程度上存在矛盾,需平衡:
整潔架構非銀彈,容易過度設計,學習門檻較高。
多倉庫遷移路線:數據為支撐,測試用例先行,借助自動代碼轉換工具和視覺輔助UI自動化測試,制定合理的灰度策略并建立及時的故障反饋和響應渠道。對于大型復雜項目或模塊,先進行面向遷移的重構,也能起到事半功倍的作用。
對我們而言,遷移的結束只是起點,基于更整潔的架構和更先進的前端框架,未來仍有很多發力點:
本文鏈接:http://www.tebozhan.com/showinfo-26-99167-0.html我們一起聊聊審核平臺前端新老倉庫遷移
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com