自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

我們一起聊聊審核平臺(tái)前端新老倉(cāng)庫(kù)遷移

開發(fā) 前端
配置化詳情頁(yè)采用的是業(yè)務(wù)定制化的低代碼方案,包含schema渲染器和任務(wù)流兩部分。當(dāng)前已沉淀近百份json schema,托管在內(nèi)部其他低代碼平臺(tái)上。頁(yè)面覆蓋40多個(gè)業(yè)務(wù),占據(jù)平臺(tái)約20%訪問量和35%獨(dú)立訪客。

背景

審核平臺(tái)接入50+業(yè)務(wù),提供在線審核及離線質(zhì)檢、新人培訓(xùn)等核心能力,同時(shí)提供數(shù)據(jù)報(bào)表、資源追蹤、知識(shí)庫(kù)等工具。隨著平臺(tái)的飛速發(fā)展,越來(lái)越多的新業(yè)務(wù)正在或即將接入審核平臺(tái),日均頁(yè)面瀏覽量為百萬(wàn)級(jí)別。如今審核平臺(tái)已是公司內(nèi)容生產(chǎn)鏈路上的關(guān)鍵一環(huán),是保障內(nèi)容安全的重要防線,因此穩(wěn)定性至關(guān)重要。

過去一年我們?cè)鴮?duì)前端項(xiàng)目進(jìn)行框架升級(jí),考慮風(fēng)險(xiǎn)與成本最小化,選擇了漸進(jìn)式升級(jí),利用微前端實(shí)現(xiàn)Vue2和Vue3共存,新接業(yè)務(wù)在Vue3倉(cāng)庫(kù)中開發(fā)。經(jīng)過一年的迭代,Vue3項(xiàng)目趨于穩(wěn)定,沉淀了大部分通用能力。為了降低多倉(cāng)庫(kù)維護(hù)心智,同時(shí)解決核心模塊的技術(shù)債務(wù),考慮將剩余活躍代碼進(jìn)行重構(gòu)并遷移至新倉(cāng)庫(kù)。

面向遷移的重構(gòu) - 整潔架構(gòu)在前端的應(yīng)用

案例選擇

參考前端埋點(diǎn)報(bào)表,選擇老倉(cāng)庫(kù)中頁(yè)面維度訪問量最高的路由,對(duì)線上使用情況進(jìn)行摸排。日常業(yè)務(wù)現(xiàn)狀是點(diǎn)直融合,直播業(yè)務(wù)配置化接入需求較多,因?yàn)闃I(yè)務(wù)形態(tài)的差異,定制需求多,現(xiàn)有配置能力無(wú)法滿足,需擴(kuò)充。開發(fā)現(xiàn)狀是通用配置化代碼改動(dòng)頻繁,邏輯復(fù)雜,開發(fā)門檻較高,影響范圍大,牽一發(fā)而動(dòng)全身。因此選擇配置化詳情頁(yè)作為優(yōu)先重構(gòu)并遷移的對(duì)象。

配置化詳情頁(yè)采用的是業(yè)務(wù)定制化的低代碼方案,包含schema渲染器和任務(wù)流兩部分。當(dāng)前已沉淀近百份json schema,托管在內(nèi)部其他低代碼平臺(tái)上。頁(yè)面覆蓋40多個(gè)業(yè)務(wù),占據(jù)平臺(tái)約20%訪問量和35%獨(dú)立訪客。

圖片圖片

圖片圖片

如果將頁(yè)面看做一個(gè)黑盒子,依據(jù)唯一標(biāo)識(shí)(路由path和query等)從node服務(wù)、平臺(tái)服務(wù)以及外部業(yè)務(wù)方服務(wù)獲取數(shù)據(jù),基于頁(yè)面內(nèi)部規(guī)則渲染頁(yè)面。審核員瀏覽并進(jìn)行通過、駁回等操作,提交后將對(duì)視頻、彈幕等業(yè)務(wù)資源產(chǎn)生影響。

圖片圖片

schema渲染器基于json schema和接口數(shù)據(jù),在平臺(tái)內(nèi)生成路由信息與頁(yè)面內(nèi)容,負(fù)責(zé)各種模式的頁(yè)面分發(fā)、物料分發(fā),并提供敏感詞、快照、洗數(shù)等通用平臺(tái)能力。

代碼現(xiàn)狀是數(shù)據(jù)獲取、提交操作和頁(yè)面復(fù)雜邏輯分散在vue文件和store中,業(yè)務(wù)邏輯和UI框架耦合嚴(yán)重,不利于集成自動(dòng)化測(cè)試和框架升級(jí)。待辦、任務(wù)、資源等邊界劃分不清晰,平鋪在“巨石store“中,維護(hù)成本極高且代碼改動(dòng)風(fēng)險(xiǎn)大。渲染器和任務(wù)流邏輯不夠內(nèi)聚,耦合嚴(yán)重,無(wú)法做到關(guān)注點(diǎn)分離。因此需要尋找一種合適的架構(gòu)進(jìn)行重構(gòu),減弱業(yè)務(wù)邏輯對(duì)UI框架的依賴,增強(qiáng)可測(cè)試性。

整潔架構(gòu)

整潔架構(gòu)由Robert C. Martin在2012年提出,核心思想是將軟件系統(tǒng)拆分為獨(dú)立的層次,以實(shí)現(xiàn)高內(nèi)聚、低耦合、可測(cè)試和可維護(hù)。

圖片圖片

一共分為四個(gè)層級(jí),環(huán)與環(huán)之間,存在一個(gè)依賴關(guān)系原則:源代碼中的依賴關(guān)系,必須只指向同心圓的內(nèi)層,即由低層機(jī)制指向高級(jí)策略。

  • 實(shí)體層:包含業(yè)務(wù)領(lǐng)域的核心概念和業(yè)務(wù)邏輯。
  • 用例層:實(shí)現(xiàn)特定的業(yè)務(wù)用例,將實(shí)體層的業(yè)務(wù)邏輯與具體的應(yīng)用場(chǎng)景結(jié)合起來(lái)。
  • 接口適配器層:負(fù)責(zé)處理與外部系統(tǒng)的交互比如用戶界面、數(shù)據(jù)庫(kù)、web服務(wù)等。將外部系統(tǒng)的請(qǐng)求和數(shù)據(jù)格式轉(zhuǎn)化為用例層和實(shí)體層能夠理解和處理的對(duì)象。
  • 框架和驅(qū)動(dòng)層:包含具體的框架和工具,比如web框架、數(shù)據(jù)庫(kù)驅(qū)動(dòng)等。

優(yōu)點(diǎn)是可以在沒有UI、數(shù)據(jù)庫(kù)、web服務(wù)器或其他外部基礎(chǔ)設(shè)施的情況下測(cè)試業(yè)務(wù)邏輯;降低對(duì)UI框架的依賴,比如跨端開發(fā)時(shí),業(yè)務(wù)邏輯可以復(fù)用,只需要做UI層的適配。相應(yīng)的,缺點(diǎn)也很明顯,過于復(fù)雜,數(shù)據(jù)需要經(jīng)過多層處理。學(xué)習(xí)成本較高,容易過度設(shè)計(jì),增加復(fù)雜性,靈活性較低。適用于大型復(fù)雜項(xiàng)目,對(duì)于需要長(zhǎng)期維護(hù)和持續(xù)開發(fā)的項(xiàng)目,清晰層次結(jié)構(gòu)和明確依賴關(guān)系有助于減少代碼腐化,更容易適應(yīng)需求變化。針對(duì)我們選擇的模塊,審核前端配置化頁(yè)面,比較適合。

重構(gòu)

圖片圖片

圖片圖片

實(shí)體層

圖片圖片

主要可拆分成待辦、任務(wù)、資源等實(shí)體。待辦實(shí)體,主要提供待辦的基礎(chǔ)信息、獲取配置等屬性和方法。任務(wù)實(shí)體提供任務(wù)的狀態(tài)、任務(wù)耗時(shí)、調(diào)度配置、任務(wù)數(shù)據(jù),計(jì)時(shí)和拉取任務(wù)流程等。資源實(shí)體提供資源的詳情數(shù)據(jù)、獲取詳情及數(shù)據(jù)清洗方法等。待辦實(shí)體包含了配置詳情頁(yè)所需的核心數(shù)據(jù),任務(wù)實(shí)體高度抽象了核心任務(wù)流。在新業(yè)務(wù)接入過程中,實(shí)體層一般不變動(dòng),通過依賴倒置劃分架構(gòu)邊界。

// entities/todo.js
export default class Todo {
    todoId
    businessId
    todoConfig
    ...
    constructor() {}
    async getTodoConfig() {
        // 獲取配置
    }
}


// entities/task.js
export default class Task {
  dispatch_conf
  listData
  timeCount
  ...
  constructor() {}
  async getTaskDetail({ getTask, taskFormat, afterGetTask}) {
    // 抽象封裝核心任務(wù)流程
  }
  // 計(jì)時(shí)邏輯
  startTimer() {}
  clearTimers() {}
}


// entities/resource.js
export default class Resource {
    detail
    dataReady
    constructor() {}
    async getResourceDetail({ getResource, resourceFormat, afterGetResource }) {
        // 抽象封裝資源模式核心流程
    }
}

用例層

用例層針對(duì)洗數(shù)、提交等復(fù)雜場(chǎng)景,通過調(diào)用實(shí)體層來(lái)實(shí)現(xiàn)特定的業(yè)務(wù)邏輯,是適配器層與實(shí)體層的中介。例如封裝了基于配置的洗數(shù)中間件,列表模式的單個(gè)和批量提交,卡片模式的單個(gè)和批量提交,快照上報(bào)、自動(dòng)化質(zhì)檢的復(fù)雜邏輯。用例層包含了系統(tǒng)的復(fù)雜業(yè)務(wù)邏輯,為單元測(cè)試提供了便利,可以獨(dú)立于UI和外部系統(tǒng)進(jìn)行測(cè)試。

// usecase/use-single-submit
import { get } from 'lodash-es' // 三方工具庫(kù)
import { ANNOTATION_SINGLE_OPER_PASS_NAME } from '@/constants' // 常量
import { workbenchApi } from '@/api'
import { setLogData } from '@/utils/xx' // 工具函數(shù)
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() // 調(diào)用鉤子函數(shù)
    }
  })
}

適配器層

適配器層包含store和UI,調(diào)用用例層的代碼,主要負(fù)責(zé)將依賴UI、外部服務(wù)、設(shè)備等的數(shù)據(jù)處理為用例層可以使用的“干凈數(shù)據(jù)”。適配器層一般不包括復(fù)雜的業(yè)務(wù)邏輯,因此在框架遷移時(shí)僅需關(guān)注基本的框架差異,適合自動(dòng)化代碼轉(zhuǎn)換。

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

新業(yè)務(wù)接入一般對(duì)實(shí)體層和用例層無(wú)改動(dòng),僅需適配器層增加相應(yīng)的展示物料,盡可能避免“牽一發(fā)動(dòng)全身”。新架構(gòu)會(huì)有一定的初學(xué)成本,但結(jié)合審核平臺(tái)復(fù)雜的項(xiàng)目現(xiàn)狀和持續(xù)接入新業(yè)務(wù)的節(jié)奏,長(zhǎng)遠(yuǎn)來(lái)看對(duì)于系統(tǒng)穩(wěn)定性、可測(cè)試性有一定幫助,同時(shí)降低UI框架依賴性。

基于新的架構(gòu),可分層進(jìn)行自動(dòng)化測(cè)試和自動(dòng)代碼轉(zhuǎn)換。

完善用例層的單元測(cè)試

用例層為純函數(shù),不依賴框架、設(shè)備、三方服務(wù)等。單元測(cè)試的技術(shù)棧為jest和vue-test-utils,從審核員的基本工作模式入手,針對(duì)任務(wù)領(lǐng)取、數(shù)據(jù)清洗與展示、稿件處理三個(gè)環(huán)節(jié)完善測(cè)試用例。因?yàn)槭切吕蟼}(cāng)庫(kù)遷移,所以可以將線上環(huán)境視為基準(zhǔn)進(jìn)行用例采集。根據(jù)業(yè)務(wù)重要性、線上訪問情況,按優(yōu)先級(jí)執(zhí)行測(cè)試。單測(cè)能有效降低回歸成本,在新業(yè)務(wù)持續(xù)接入的背景下,保障系統(tǒng)穩(wěn)定性。

適配器層的自動(dòng)代碼轉(zhuǎn)換 - 基于gogocode將vue2升級(jí)為vue3 setup語(yǔ)法

調(diào)研與分析

在尋求升級(jí)方案的過程中,我們對(duì)比了兩款工具:Gogocode 和 vue2-to-composition-api。以下是它們的簡(jiǎn)要對(duì)比:

功能

Gogocode

vue2-to-composition-api

缺點(diǎn)

默認(rèn)不支持轉(zhuǎn)換成 Vue3 setup 語(yǔ)法

不支持 template 轉(zhuǎn)換

轉(zhuǎn)換規(guī)則覆蓋

轉(zhuǎn)換規(guī)則列表

轉(zhuǎn)換效果

特性

像 jQuery 一樣修改AST

將 Vue2 代碼轉(zhuǎn)換為 Vue 3 的 Composition API 格式


2. jQuery-like API 簡(jiǎn)化了 AST 修改成本

2. 支持在線轉(zhuǎn)換

優(yōu)點(diǎn)

1. 支持自定義插件

1. 支持轉(zhuǎn)換成 Vue3 setup 語(yǔ)法

經(jīng)過調(diào)研,gogocode 是基于 AST 封裝的庫(kù)可擴(kuò)展空間大,但是默認(rèn) gogocode-plugin-vue 不支持轉(zhuǎn)換成 Composition API

vue2-to-composition-api 倒是支持 Composition API,但是不支持 template 部分的轉(zhuǎn)換。

考慮到我們的項(xiàng)目中有許多自定義的轉(zhuǎn)換邏輯,如 UI 庫(kù)替換、store 替換等,我們最終決定使用 gogocode 作為主要工具,并結(jié)合其他手段來(lái)實(shí)現(xiàn) vue2 到 vue3 的全面升級(jí)。

實(shí)施與探索

基礎(chǔ):升級(jí)語(yǔ)法

升級(jí)語(yǔ)法是借助 gogocode 的 replace 方法實(shí)現(xiàn)的,通過 $$$ 匹配符保留所需要的代碼塊,將 Vue2 的語(yǔ)法快速替換成 Vue3 的語(yǔ)法。

// 替換data
scriptAst.replace("data() {return {$$$};}", `const $data = reactive({$$$})`);
// 替換props
scriptAst.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 替換的方式適合大部分場(chǎng)景,比如 methods、filters、watch 等

圖片圖片

進(jìn)階:處理模板中的變量、函數(shù)中的this變量

template綁定的變量可能是data,可能是props,還可能是 methods

想要替換 template 中的變量,需要先收集 data、props、methods 中的 keys

getDataKeys() {
  const keys = new Set();
  // 只需要第一層的key,所以deep設(shè)為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 有點(diǎn)小復(fù)雜,需要考慮異步函數(shù),普通函數(shù)和鍵值對(duì)的寫法
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 替換也是一個(gè)繁瑣的問題,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調(diào)用
    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 中的 動(dòng)態(tài)methods調(diào)用
  code = code.replace(/this\[(.+)\]/g, (match, $1) => {
    return `methods[${$1}]`;
  })
  return code
}

進(jìn)階:動(dòng)態(tài)調(diào)用this.xxx該如何解決

async parseOpenDialog(payload) {
  const { schema, data } = payload
  await this[schema.method](
    parseSchema,
    data,
    dialogParams,
    dialogType
  )
}

按 vue3 新的寫法,一般是展開的

const parseOpenDialog = async (payload) => {
}

但是按這個(gè)習(xí)慣來(lái)轉(zhuǎn)換,就無(wú)法做到動(dòng)態(tài)調(diào)用this.xxx,我們可以嘗試把方法都放在methods對(duì)象中,有點(diǎn)類似 vue2

const methods = {
  parseOpenDialog: async (payload) => {
  },
  xxx: async() => {
  }
}

在替換 this 時(shí),將 this 替換成 methods 變成 methods[schema.method]()。

其他技巧

1 . 原來(lái) vue2 中肯很多屬性掛在組件實(shí)例上,比如 $route, $router, $emit 甚至自定義的屬性等等。下面是 vue3 中獲取組件實(shí)例的方法。

import { getCurrentInstance } from 'vue'
const { proxy: $vm } = getCurrentInstance()


$vm.xxx = '自定義屬性'

2 . 原來(lái)使用的 vuex,現(xiàn)在使用的是 pinia。我們需要先收集 ...mapState($_$, [$$$1]),然后使用 storeToRefs 代替

// 獲取store 使用storeToRefs
if (this.storeType === 'pinia') {
  this.scriptAst
  .find("computed:{}")
  .before(`const {${stateNames.join(',')}} = storeToRefs(${this.getPiniaStoreName(key)})`)
  .replace("computed:{$$$}", "$$$")
}

最終將上述轉(zhuǎn)換能力封裝成一個(gè)庫(kù),通過 npm 安裝來(lái)實(shí)現(xiàn)組件批量升級(jí)。

遷移前后的E2E測(cè)試 - 視覺輔助UI自動(dòng)化測(cè)試

端到端測(cè)試是確保新舊系統(tǒng)平穩(wěn)過渡的關(guān)鍵步驟。本次遷移依舊遵循漸進(jìn)式升級(jí)的原則,新增v3路由,線上新老路由共存。共分為功能測(cè)試、UI測(cè)試、性能測(cè)試三個(gè)部分。功能測(cè)試為單元測(cè)試的補(bǔ)充,主要驗(yàn)證新老路由下核心操作路徑及提交參數(shù)的一致性,攔截請(qǐng)求避免對(duì)線上造成影響。性能由通用監(jiān)控大盤進(jìn)行保障。UI對(duì)比測(cè)試是本次的重點(diǎn)。

新老倉(cāng)庫(kù)分別基于Element UI和Element Plus,Element Plus重新設(shè)計(jì)了組件以適應(yīng)Vue3,組件尺寸體系調(diào)整為更自然的大中小選項(xiàng)。間距優(yōu)化為更通用的4px體系,主要涉及 padding 和 margin 屬性修改、 font-size 等字體和圖標(biāo)大小修改等。因此,雖然大部分組件在外觀上保持相似,視覺和布局上可能有一些差異。由于業(yè)務(wù)組件具備一定復(fù)雜性,手寫測(cè)試用例工作量繁瑣,新舊頁(yè)面組件可能存在差異無(wú)法完全復(fù)用,方案也不具備通用性。因此考慮使用計(jì)算機(jī)視覺技術(shù)來(lái)識(shí)別和驗(yàn)證用戶界面的元素。

基于公司內(nèi)部的自動(dòng)化測(cè)試平臺(tái),測(cè)試框架為Playwright,測(cè)試語(yǔ)言選擇Python以更好的利用豐富的圖像處理庫(kù)。指定CSS選擇器,隨機(jī)選擇頁(yè)面上的元素進(jìn)行截圖和對(duì)比,設(shè)定閾值進(jìn)行判斷。圖像相似度對(duì)比可分為傳統(tǒng)的基于像素差的方法和基于圖像特征的方法。圖像特征有SIFT、ORB等特征提取方法和深度學(xué)習(xí)方法。分別選擇一種代表性的算法進(jìn)行對(duì)比和測(cè)試。

  • SSIM(結(jié)構(gòu)相似性指數(shù)),同時(shí)考慮圖片亮度、對(duì)比度與結(jié)構(gòu)信息。用于檢測(cè)兩張相同尺寸的圖像的相似性。對(duì)于圖像的亮度和對(duì)比度變化具有較好的魯棒性。
  • SIFT(尺度不變特征變換),用于在圖像中檢測(cè)和描述局部特征,這些特征對(duì)圖像的縮放、旋轉(zhuǎn)和部分亮度變化具有不變性。能夠檢測(cè)和匹配不同尺寸下的特征,對(duì)圖像的仿射變換、噪聲和部分遮擋具有較好的魯棒性。
  • LPIPS是一種深度學(xué)習(xí)特征,用于量化圖像之間的感知相似性,通過比較圖像在深度神經(jīng)網(wǎng)絡(luò)中的特征表示來(lái)工作,這些特征能夠捕捉到人類視覺所關(guān)注的圖像細(xì)節(jié)?;谝粋€(gè)大規(guī)模的人類感知相似性判斷數(shù)據(jù)集,包含484K個(gè)人類的判斷,涵蓋了多種圖像變換和失真類型。使用深度特征(例如VGG網(wǎng)絡(luò)中的特征)來(lái)衡量圖像相似性,比傳統(tǒng)的度量方法更有效。

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

實(shí)驗(yàn)表明,基于深度學(xué)習(xí)特征的相似度對(duì)比結(jié)果更接近用戶感知,針對(duì)Vue2升級(jí)Vue3 UI組件庫(kù)導(dǎo)致的間距、字體、尺寸等細(xì)微差異判斷更準(zhǔn)確。因此采用LPIPS作為對(duì)比算法。

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

依據(jù)業(yè)務(wù)流程,分別訪問新老路由,對(duì)頁(yè)面指定元素進(jìn)行截圖、對(duì)比和拼接,輸出測(cè)試截圖和完整的測(cè)試報(bào)告。

圖片圖片

圖片圖片

采用視覺輔助UI自動(dòng)化測(cè)試,更接近用戶真實(shí)感知,大大降低測(cè)試用例復(fù)雜度,提升測(cè)試效率,無(wú)需關(guān)注繁雜的DOM元素層級(jí)。

上線

以頁(yè)面配置的形式按待辦進(jìn)行灰度,跳轉(zhuǎn)至新路由。單元測(cè)試已集成至項(xiàng)目流水線中,MR和發(fā)布前觸發(fā)。灰度過程中手動(dòng)執(zhí)行E2E用例,以自定義環(huán)境變量的形式指定頁(yè)面路徑、元素選擇器、相似度閾值等。測(cè)試通過后修改頁(yè)面配置,引流至新路由。通過用戶故障群和頁(yè)面反饋入口響應(yīng)用戶反饋,結(jié)合前端埋點(diǎn)報(bào)表觀察線上使用情況和確定灰度策略。通過完善單元測(cè)試、E2E測(cè)試和制定合理的灰度策略,針對(duì)特定模板的遷移已順利完成,期間未收到線上故障反饋。

后續(xù)遷移策略將以老倉(cāng)庫(kù)中改動(dòng)較頻繁文件優(yōu)先入手,測(cè)試用例先行,借助自動(dòng)代碼轉(zhuǎn)換工具快速平穩(wěn)遷移,線上埋點(diǎn)數(shù)據(jù)做輔助。

收益

活躍代碼陸續(xù)遷移,結(jié)束多倉(cāng)庫(kù)并行,減少維護(hù)心智。之前我們的常態(tài)是新老倉(cāng)庫(kù)并行,開發(fā)一個(gè)完整的業(yè)務(wù)功能時(shí)要在ts和js,選項(xiàng)式API和setup語(yǔ)法之間頻繁切換,心智負(fù)擔(dān)較重?;钴S代碼陸續(xù)遷移至vue3新倉(cāng)庫(kù),結(jié)合新的框架特性和實(shí)用工具,能夠更專注于業(yè)務(wù)邏輯本身。

圖片圖片

核心模塊重構(gòu),“巨石store”輕量化,提高可維護(hù)性和可演進(jìn)性,分層結(jié)構(gòu)保障核心邏輯穩(wěn)定性。原本配置能力擴(kuò)充時(shí)需要在復(fù)雜的數(shù)據(jù)流中“走迷宮”,耦合嚴(yán)重,通用代碼影響范圍大,常常出現(xiàn)A業(yè)務(wù)需求上線導(dǎo)致B業(yè)務(wù)不可用的情況。利用整潔架構(gòu)進(jìn)行分層設(shè)計(jì)后,新增一種審核模式僅需在適配器層新增對(duì)應(yīng)物料和action,用例層新增用例。無(wú)需修改實(shí)體層和其他業(yè)務(wù)相關(guān)的用例、通用頁(yè)面、物料等。

圖片圖片

圖片圖片

業(yè)務(wù)邏輯的重新梳理,彌補(bǔ)測(cè)試用例空缺。領(lǐng)域提取是對(duì)業(yè)務(wù)邏輯的重新梳理,前端能加深業(yè)務(wù)理解。穩(wěn)定性至上的模塊很長(zhǎng)時(shí)間缺少測(cè)試用例,造成對(duì)開發(fā)人員的經(jīng)驗(yàn)、能力依賴極大。完善核心鏈路的測(cè)試用例能有效降低回歸成本,保障系統(tǒng)穩(wěn)定性。

工具沉淀,組內(nèi)復(fù)用。自動(dòng)代碼轉(zhuǎn)換工具和基于AI能力的前端E2E測(cè)試方案為后續(xù)組內(nèi)其他項(xiàng)目的框架升級(jí)和遷移提供了便利。

總結(jié)與展望

在2023年開始漸進(jìn)式升級(jí)Vue3后,我們經(jīng)歷了很長(zhǎng)一段時(shí)間的多倉(cāng)庫(kù)并行。在新業(yè)務(wù)不斷接入、開發(fā)新成員加入的背景下,這樣的模式無(wú)疑提升了開發(fā)門檻和維護(hù)心智。本次活躍代碼的陸續(xù)遷移結(jié)束了多倉(cāng)庫(kù)并行的現(xiàn)狀,同時(shí)在整個(gè)實(shí)踐過程中我們?yōu)閷徍似脚_(tái)這個(gè)大型復(fù)雜項(xiàng)目前端引入了整潔架構(gòu)的思想,為后續(xù)的開發(fā)維護(hù)提供了一種新的思路。沉淀了一套自動(dòng)化vue2代碼轉(zhuǎn)vue3 setup的工具,可為后臺(tái)項(xiàng)目的框架升級(jí)提供便利。同時(shí)借助AI能力提升前端E2E測(cè)試的效率,利用計(jì)算機(jī)視覺輔助前端UI自動(dòng)化測(cè)試。有幾點(diǎn)心得:

完美重構(gòu)、敏捷重構(gòu)、系統(tǒng)穩(wěn)定性難以平衡。

圖片圖片

既然下定決心對(duì)年久失修的代碼進(jìn)行重構(gòu),我們一定是追求極致優(yōu)化和整潔的。但是需求現(xiàn)狀是不斷有新特性進(jìn)來(lái),戰(zhàn)線拉的太長(zhǎng)必將導(dǎo)致抹平差異的成本增加,因此敏捷性也很重要。同時(shí)底線是關(guān)注系統(tǒng)的可靠性和穩(wěn)定性。這三者一定程度上存在矛盾,需平衡:

  • 任務(wù)拆分,基于埋點(diǎn)數(shù)據(jù)選擇最重要或最緊急的鏈路進(jìn)行重構(gòu),而非追求大而全,先落地架構(gòu),后擴(kuò)充功能
  • 測(cè)試用例先行,重構(gòu)必將引入風(fēng)險(xiǎn),用例先行能最大程度保障核心功能的穩(wěn)定遷移
  • 制定合理的灰度策略,新增路由,以頁(yè)面配置形式進(jìn)行灰度,優(yōu)先uv較低的頁(yè)面驗(yàn)證,并保證及時(shí)的反饋渠道
  • 持續(xù)優(yōu)化,重構(gòu)應(yīng)成為一種思想,在迭代中持續(xù)優(yōu)化

整潔架構(gòu)非銀彈,容易過度設(shè)計(jì),學(xué)習(xí)門檻較高。

  • 按模塊重構(gòu),而非項(xiàng)目
  • 針對(duì)新架構(gòu),制定長(zhǎng)期維護(hù)計(jì)劃,組織團(tuán)隊(duì)培訓(xùn),降低學(xué)習(xí)成本

多倉(cāng)庫(kù)遷移路線:數(shù)據(jù)為支撐,測(cè)試用例先行,借助自動(dòng)代碼轉(zhuǎn)換工具和視覺輔助UI自動(dòng)化測(cè)試,制定合理的灰度策略并建立及時(shí)的故障反饋和響應(yīng)渠道。對(duì)于大型復(fù)雜項(xiàng)目或模塊,先進(jìn)行面向遷移的重構(gòu),也能起到事半功倍的作用。

對(duì)我們而言,遷移的結(jié)束只是起點(diǎn),基于更整潔的架構(gòu)和更先進(jìn)的前端框架,未來(lái)仍有很多發(fā)力點(diǎn):

  • 對(duì)于遷移過程中沉淀下來(lái)的新架構(gòu),需不斷優(yōu)化和改進(jìn),以適應(yīng)復(fù)雜的業(yè)務(wù)場(chǎng)景。
  • 在更多的業(yè)務(wù)場(chǎng)景下評(píng)估遷移后的性能改進(jìn),確保用戶體驗(yàn)得到提升。
  • 持續(xù)審查并解決在遷移過程中發(fā)現(xiàn)的技術(shù)債務(wù)。
  • 持續(xù)建設(shè)自動(dòng)代碼轉(zhuǎn)換工具,賦能團(tuán)隊(duì)內(nèi)其他項(xiàng)目。
  • 視覺輔助UI自動(dòng)化測(cè)試的方案,進(jìn)一步抽象,給到不熟悉自動(dòng)化測(cè)試的團(tuán)隊(duì)成員“開箱即用”。
  • ...
責(zé)任編輯:武曉燕 來(lái)源: 嗶哩嗶哩技術(shù)
相關(guān)推薦

2023-11-29 09:04:00

前端接口

2023-03-29 08:26:06

2021-08-27 07:06:10

IOJava抽象

2024-02-20 21:34:16

循環(huán)GolangGo

2023-08-04 08:20:56

DockerfileDocker工具

2022-05-24 08:21:16

數(shù)據(jù)安全API

2023-08-10 08:28:46

網(wǎng)絡(luò)編程通信

2023-09-10 21:42:31

2023-06-30 08:18:51

敏捷開發(fā)模式

2025-03-13 05:00:00

2022-02-14 07:03:31

網(wǎng)站安全MFA

2022-06-26 09:40:55

Django框架服務(wù)

2022-10-28 07:27:17

Netty異步Future

2023-04-26 07:30:00

promptUI非結(jié)構(gòu)化

2022-04-06 08:23:57

指針函數(shù)代碼

2023-12-28 09:55:08

隊(duì)列數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)

2022-11-12 12:33:38

CSS預(yù)處理器Sass

2024-02-26 00:00:00

Go性能工具

2023-07-27 07:46:51

SAFe團(tuán)隊(duì)測(cè)試

2025-03-27 02:00:00

SPIJava接口
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)