審核平臺前端新老倉庫遷移

背景

審核平臺接入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年提出,核心思想是將軟件系統拆分為獨立的層次,以實現高內聚、低耦合、可測試和可維護。

圖片

一共分為四個層級,環與環之間,存在一個依賴關系原則:源代碼中的依賴關系,必須只指向同心圓的內層,即由低層機制指向高級策略。

  • 實體層:包含業務領域的核心概念和業務邏輯。

  • 用例層:實現特定的業務用例,將實體層的業務邏輯與具體的應用場景結合起來。

  • 接口適配器層:負責處理與外部系統的交互比如用戶界面、數據庫、web服務等。將外部系統的請求和數據格式轉化為用例層和實體層能夠理解和處理的對象。

  • 框架和驅動層:包含具體的框架和工具,比如web框架、數據庫驅動等。

優點是可以在沒有UI、數據庫、web服務器或其他外部基礎設施的情況下測試業務邏輯;降低對UI框架的依賴,比如跨端開發時,業務邏輯可以復用,只需要做UI層的適配。相應的,缺點也很明顯,過于復雜,數據需要經過多層處理。學習成本較高,容易過度設計,增加復雜性,靈活性較低。適用于大型復雜項目,對于需要長期維護和持續開發的項目,清晰層次結構和明確依賴關系有助于減少代碼腐化,更容易適應需求變化。針對我們選擇的模塊,審核前端配置化頁面,比較適合。

重構

圖片

圖片

實體層

圖片

主要可拆分成待辦、任務、資源等實體。待辦實體,主要提供待辦的基礎信息、獲取配置等屬性和方法。任務實體提供任務的狀態、任務耗時、調度配置、任務數據,計時和拉取任務流程等。資源實體提供資源的詳情數據、獲取詳情及數據清洗方法等。待辦實體包含了配置詳情頁所需的核心數據,任務實體高度抽象了核心任務流。在新業務接入過程中,實體層一般不變動,通過依賴倒置劃分架構邊界。

// entities/todo.js
export default class Todo {todoIdbusinessIdtodoConfig...constructor() {}async getTodoConfig() {// 獲取配置}
}// entities/task.js
export default class Task {dispatch_conflistDatatimeCount...constructor() {}async getTaskDetail({ getTask, taskFormat, afterGetTask}) {// 抽象封裝核心任務流程}// 計時邏輯startTimer() {}clearTimers() {}
}// entities/resource.js
export default class Resource {detaildataReadyconstructor() {}async getResourceDetail({ getResource, resourceFormat, afterGetResource }) {// 抽象封裝資源模式核心流程}
}
 

用例層

用例層針對洗數、提交等復雜場景,通過調用實體層來實現特定的業務邏輯,是適配器層與實體層的中介。例如封裝了基于配置的洗數中間件,列表模式的單個和批量提交,卡片模式的單個和批量提交,快照上報、自動化質檢的復雜邏輯。用例層包含了系統的復雜業務邏輯,為單元測試提供了便利,可以獨立于UI和外部系統進行測試。

// usecase/use-single-submit
import { 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/todoConfigDetail
import { 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.queryconst 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.vue
const todoConfigDetailStore = useTodoConfigDetailStore()
const { todo, task, multipleSelection } = storeToRefs(todoConfigDetailStore)
const { getTaskDetail, submit } = todoConfigDetailStore

新業務接入一般對實體層和用例層無改動,僅需適配器層增加相應的展示物料,盡可能避免“牽一發動全身”。新架構會有一定的初學成本,但結合審核平臺復雜的項目現狀和持續接入新業務的節奏,長遠來看對于系統穩定性、可測試性有一定幫助,同時降低UI框架依賴性。

基于新的架構,可分層進行自動化測試和自動代碼轉換。

完善用例層的單元測試

用例層為純函數,不依賴框架、設備、三方服務等。單元測試的技術棧為jest和vue-test-utils,從審核員的基本工作模式入手,針對任務領取、數據清洗與展示、稿件處理三個環節完善測試用例。因為是新老倉庫遷移,所以可以將線上環境視為基準進行用例采集。根據業務重要性、線上訪問情況,按優先級執行測試。單測能有效降低回歸成本,在新業務持續接入的背景下,保障系統穩定性。

適配器層的自動代碼轉換 -?基于gogocode將vue2升級為vue3 setup語法

調研與分析

在尋求升級方案的過程中,我們對比了兩款工具: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}

進階:動態調用this.xxx該如何解決

???????

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等特征提取方法和深度學習方法。分別選擇一種代表性的算法進行對比和測試。

  • SSIM(結構相似性指數),同時考慮圖片亮度、對比度與結構信息。用于檢測兩張相同尺寸的圖像的相似性。對于圖像的亮度和對比度變化具有較好的魯棒性。

  • SIFT(尺度不變特征變換),用于在圖像中檢測和描述局部特征,這些特征對圖像的縮放、旋轉和部分亮度變化具有不變性。能夠檢測和匹配不同尺寸下的特征,對圖像的仿射變換、噪聲和部分遮擋具有較好的魯棒性。

  • LPIPS是一種深度學習特征,用于量化圖像之間的感知相似性,通過比較圖像在深度神經網絡中的特征表示來工作,這些特征能夠捕捉到人類視覺所關注的圖像細節。基于一個大規模的人類感知相似性判斷數據集,包含484K個人類的判斷,涵蓋了多種圖像變換和失真類型。使用深度特征(例如VGG網絡中的特征)來衡量圖像相似性,比傳統的度量方法更有效。

v2v3SSIMSIFTLPIPS

圖片

圖片

0.6560.8550.909

圖片

圖片

0.9160.8740.961

圖片

圖片

0.6580.8570.881

圖片

圖片

0.8770.8550.926

圖片

圖片

0.5510.8360.947

圖片

圖片

0.9290.8840.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自動化測試。有幾點心得:

完美重構、敏捷重構、系統穩定性難以平衡。

圖片

既然下定決心對年久失修的代碼進行重構,我們一定是追求極致優化和整潔的。但是需求現狀是不斷有新特性進來,戰線拉的太長必將導致抹平差異的成本增加,因此敏捷性也很重要。同時底線是關注系統的可靠性和穩定性。這三者一定程度上存在矛盾,需平衡:

  • 任務拆分,基于埋點數據選擇最重要或最緊急的鏈路進行重構,而非追求大而全,先落地架構,后擴充功能

  • 測試用例先行,重構必將引入風險,用例先行能最大程度保障核心功能的穩定遷移

  • 制定合理的灰度策略,新增路由,以頁面配置形式進行灰度,優先uv較低的頁面驗證,并保證及時的反饋渠道

  • 持續優化,重構應成為一種思想,在迭代中持續優化

整潔架構非銀彈,容易過度設計,學習門檻較高。

  • 按模塊重構,而非項目

  • 針對新架構,制定長期維護計劃,組織團隊培訓,降低學習成本

多倉庫遷移路線:數據為支撐,測試用例先行,借助自動代碼轉換工具和視覺輔助UI自動化測試,制定合理的灰度策略并建立及時的故障反饋和響應渠道。對于大型復雜項目或模塊,先進行面向遷移的重構,也能起到事半功倍的作用。

對我們而言,遷移的結束只是起點,基于更整潔的架構和更先進的前端框架,未來仍有很多發力點:

  • 對于遷移過程中沉淀下來的新架構,需不斷優化和改進,以適應復雜的業務場景。

  • 在更多的業務場景下評估遷移后的性能改進,確保用戶體驗得到提升。

  • 持續審查并解決在遷移過程中發現的技術債務。

  • 持續建設自動代碼轉換工具,賦能團隊內其他項目。

  • 視覺輔助UI自動化測試的方案,進一步抽象,給到不熟悉自動化測試的團隊成員“開箱即用”。

  • ...

-End-

作者丨伍月、莫小謙

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/40668.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/40668.shtml
英文地址,請注明出處:http://en.pswp.cn/web/40668.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

代碼提交錯分支了怎么辦?

你有么有遇到過正在開發的代碼&#xff0c;提交到生產環境的分支去&#xff0c;遇到這種情況怎么辦&#xff1f; 問題重現&#xff1a; 這段注釋// AAAAAAAAAAA 本來應該寫在dev分支的&#xff0c;現在提交并push到master分支了 現在第一步&#xff0c;撤回提交 第二步&…

第1章 認識 Vite

明白了&#xff0c;這里是第1章內容的詳細展開版本&#xff1a; 第1章 認識 Vite 1 . 什么是 Vite Vite 是一個由尤雨溪&#xff08;Vue.js 的創始人&#xff09;開發的前端構建工具&#xff0c;旨在提供極快的開發體驗。Vite 的名字來源于法語&#xff0c;意為“快速”&…

python繪制一維離散點

在Python中&#xff0c;繪制一維離散點通常意味著我們要在一條直線上標記出幾個特定的點。這可以通過多種庫來實現&#xff0c;但最常見和強大的庫之一是matplotlib。以下是一個詳細的代碼示例&#xff0c;它展示了如何使用matplotlib庫來繪制一維離散點&#xff0c;并且這個示…

C++語言常見錯誤分析匯總

在一個工程里出現兩個main函數時 3.obj : error LNK2005: _main already defined in file1.obj Debug/HELLO.exe : fatal error LNK1169: one or more multiply defined symbols found 這個就是說&#xff0c;你的main函數重定義了。你看看是不是你的工程里面&#xff0c;包…

MySQL的Geometry數據處理之WKB方案

MySQL的Geometry數據處理之WKT方案&#xff1a;https://blog.csdn.net/qq_42402854/article/details/140134357 MySQL的Geometry數據處理之WKT方案中&#xff0c;介紹WTK方案的優點&#xff0c;也感受到它的繁瑣和缺陷。比如&#xff1a; 需要借助 ST_GeomFromText和 ST_AsTex…

Spring @Cacheable緩存注解用法說明

注解Cacheable 是 Spring 框架中用于緩存數據的方法或類的注解。通過使用這個注解&#xff0c;你可以避免重復計算和重復獲取數據&#xff0c;從而提高應用程序的性能。 基本用法 引入依賴 確保在你的項目中引入了 Spring Cache 相關的依賴。如果你使用的是 Spring Boot&…

中英雙語介紹中國的城市:上海市(Shanghai)

中文版 上海市是中國最大的城市之一&#xff0c;也是全球重要的金融、貿易和航運中心。作為一座現代化的國際大都市&#xff0c;上海以其繁華的商業區、豐富的文化遺產和多樣化的經濟結構而聞名。以下是對上海市的詳細介紹&#xff0c;包括其地理位置、人口、經濟、教育、文化…

qt結合vs2022安裝

進入清華大學開源軟件&#xff1a; 清華大學開源軟件鏡像站 | Tsinghua Open Source Mirror 下載完成后&#xff0c;雙擊進行安裝&#xff1a; 進入郵箱進行驗證&#xff1a; 可能是因為網絡問題&#xff0c;無法安裝。 重新安裝5.12.12版本。 安裝后啟動失敗&#xff0c;重新…

后端接口設計考慮要點

1. 接口參數校驗 入參校驗&#xff1a;確保必要參數不為空&#xff0c;限制長度和格式&#xff08;例如郵箱格式&#xff09;。返回值校驗&#xff1a;確定返回值不為空&#xff0c;為空時返回與前端協商的默認值。 2. 接口擴展性 設計通用接口而非僅針對特定業務流程的接口…

橫截面交易策略:概念與示例

數量技術宅團隊在CSDN學院推出了量化投資系列課程 歡迎有興趣系統學習量化投資的同學&#xff0c;點擊下方鏈接報名&#xff1a; 量化投資速成營&#xff08;入門課程&#xff09; Python股票量化投資 Python期貨量化投資 Python數字貨幣量化投資 C語言CTP期貨交易系統開…

數據結構--單鏈表實現

歡迎光顧我的homepage 前言 鏈表和順序表都是線性表的一種&#xff0c;但是順序表在物理結構和邏輯結構上都是連續的&#xff0c;但鏈表在邏輯結構上是連續的&#xff0c;而在物理結構上不一定連續&#xff1b;來看以下圖片來認識鏈表與順序表的差別 這里以動態順序表…

WGAN(Wassertein GAN)

WGAN E x ~ P g [ log ? ( 1 ? D ( x ) ) ] E x ~ P g [ ? log ? D ( x ) ] \begin{aligned} & \mathbb{E}_{x \sim P_g}[\log (1-D(x))] \\ & \mathbb{E}_{x \sim P_g}[-\log D(x)] \end{aligned} ?Ex~Pg??[log(1?D(x))]Ex~Pg??[?logD(x)]? 原始 GAN …

springboot基于Java的超市進銷存系統+ LW+ PPT+源碼+講解

第三章系統分析與設計 3.1 可行性分析 一個完整的系統&#xff0c;可行性分析是必須要有的&#xff0c;因為他關系到系統生存問題&#xff0c;對開發的意義進行分析&#xff0c;能否通過本網站來補充線下超市進銷存管理模式中的缺限&#xff0c;去解決其中的不足等&#xff0c…

6域名系統DNS

《計算機網絡》第7版&#xff0c;謝希仁 每次記不清楚的知識點&#xff0c;通過上網查找&#xff0c;總是只能看到很零碎的答案。最后還是最喜歡看這個版本的書&#xff0c;一看就回憶起來了&#xff0c;邏輯嚴謹&#xff0c;循循善誘&#xff0c;知識講解的全面又清晰&#xf…

架構師應該在團隊中發揮怎樣的作用?

架構師分為5種&#xff1a; 1.企業架構師EA(Enterprise Architect) EA的職責是決定整個公司的技術路線和技術發展方向。 2.基礎結構架構師IA(Infrastructure Architect) IA的工作就是提煉和優化技術方面積累和沉淀形成的基礎性的、公共的、可復用的框架和組件&#xff0c;這…

Qt 基礎組件速學 鼠標和鍵盤事件

學習目標&#xff1a; 鼠標事件和鍵盤事件應用 前置環境 運行環境:qt creator 4.12 學習內容和效果演示&#xff1a; 1.鼠標事件 根據鼠標的坐標位置&#xff0c;做出對應的事件。 2.鍵盤事件 根據鍵盤的輸入做出對應操作 詳細主要代碼 1.鼠標事件 #include "main…

一文讀懂輕量日志收集系統Loki工作原理

Loki 是由 Grafana Labs 開發的日志聚合系統&#xff0c;設計目標是提供一種高效、低成本的日志收集和查詢解決方案。與傳統的日志系統&#xff08;如 ELK Stack&#xff09;不同&#xff0c;Loki 不會對日志內容進行索引&#xff0c;而是僅對日志的元數據進行索引&#xff0c;…

美國大帶寬服務器租用優勢和注意事項

美國大帶寬服務器租用對于需要處理大量數據和提供高速網絡服務的企業至關重要。下面將詳細討論美國大帶寬服務器租用的優勢、適用場景及注意事項&#xff0c;rak部落小編為您整理發布美國大帶寬服務器租用的優勢和注意事項。 優勢 1. 高速數據傳輸&#xff1a; - 大帶寬服務器提…

FTP、http 、tcp

HTTP VS FTP HTTP &#xff1a;HyperText Transfer Protocol 超文本傳輸協議&#xff0c;是基于TCP協議 FTP&#xff1a; File Transfer Protocol 文件傳輸協議&#xff0c; 基于TCP協議&#xff0c; 基于UDP協議的FTP 叫做 TFTP HTTP 協議 通過一個SOCKET連接傳輸依次會話數…

FIND_IN_SET使用案例--[sql語句根據多ids篩選出對應數據]

一 FIND_IN_SET select id,system_ids from intellect_client_info where FIND_IN_SET(5, system_ids) > 0;