新聞中心
一、 背景
隨著小程序的出現(xiàn),借助微信的生態(tài)體系和海量用戶,使服務(wù)以更加便捷方式的觸達(dá)用戶需求?;诖吮尘?,團(tuán)隊很早布局智能導(dǎo)購小程序(為 vivo 各個線下門店導(dǎo)購提供服務(wù)的用戶運(yùn)營工具)的開發(fā)。

早期的小程序開發(fā)工程體系還不夠健全,和現(xiàn)在的前端的工程體系相差較大,表現(xiàn)在對模塊化,組件化以及高級JavaScript 語法特性的支撐上。所以團(tuán)隊在做技術(shù)選型時,希望克服原生小程序工程體系上的不足,經(jīng)過對比最后選擇了騰訊出品的 wepy 作為整體的開發(fā)框架。
在項目的從0到1階段,wepy 確實幫助我們實現(xiàn)了快速的業(yè)務(wù)迭代,滿足線下門店導(dǎo)購的需求。但隨著時間的推移,在技術(shù)上,社區(qū)逐步沉淀出以 uniapp 為代表的 Vue 棧體系和以 Taro 為代表的 React ??缍说捏w系,wepy 目前的社區(qū)活躍度比較低。另外隨著業(yè)務(wù)進(jìn)入穩(wěn)定階段,除少量的 wepy 小程序,H5 項目和新的小程序都是基于 Vue 和 uniapp 來構(gòu)建,團(tuán)隊也是希望統(tǒng)一技術(shù)棧,實現(xiàn)更好的跨端開發(fā)能力,降低開發(fā)和維護(hù)成本,提升研發(fā)效率。
二、思考
隨著團(tuán)隊決定將智能導(dǎo)購小程序從 wepy 遷移到 uniapp 的架構(gòu)體系,我們就需要思考,如何進(jìn)行項目的平穩(wěn)的遷移,同時兼顧效率和質(zhì)量?通過對當(dāng)前的項目狀態(tài)和技術(shù)背景進(jìn)行分析,團(tuán)隊梳理出2個原則3種遷移思路。
2.1 漸進(jìn)式遷移
核心出發(fā)點,保證項目的平穩(wěn)過渡,給團(tuán)隊更多的時間,在迭代中逐步的進(jìn)行架構(gòu)遷移。希望以此來降低遷移中的風(fēng)險和不可控的點?;诖?,我們思考兩個方案:
方案一 融合兩套架構(gòu)體系
在目前的項目中引入和 uniapp 的項目體系,一個項目融合了 wepy 和 uniapp 的代碼工程化管理,逐步的將 wepy 的代碼改成 uniapp 的代碼,待遷移完成刪除 wepy 的目錄。這種方案實現(xiàn)起來不是很復(fù)雜,但是缺點是管理起來比較復(fù)雜,兩套工程化管理機(jī)制,底層的編譯機(jī)制,各種入口的配置文件等,管理起來比較麻煩。另外團(tuán)隊每個人都需要消化 wepy 到 uniapp 的領(lǐng)域知識遷移,不僅僅是項目的遷移也是知識體系的遷移。
方案二 設(shè)計 wepy-webpack-loader
以 uniapp 為工程體系基礎(chǔ),核心思路是將現(xiàn)有 wepy 代碼融入到 uniapp 的體系中來。我們都知道 uniapp 的底層依賴于 Vue 的 cli 的技術(shù)體系,最底層通過 webpack 實現(xiàn)對 Vue 單組件文件和其他資源文件的 bundle。
基于此,我們可以開發(fā)一個 wepy 的 webpack 的 loader,wepy-loader 類似于 vue-loader 的能力,通過該 loader 對 wepy 文件進(jìn)行編譯打包,然后最終輸出小程序代碼。想法很簡單,但我們想要實現(xiàn) wepy-loader工作量還是比較大的,需要對 wepy 的底層編譯器進(jìn)一步進(jìn)行分析拆解,分析 wepy 的依賴關(guān)系,區(qū)分是組件編譯還是 page 編譯等,且 wepy 底層編譯器的代碼比較復(fù)雜,實現(xiàn)成本較高。
2.2 整體性遷移
構(gòu)建一個編譯器實現(xiàn) wepy 到 uniapp 的自動代碼轉(zhuǎn)換
通過對 wepy 和 uniapp 整體技術(shù)方案的梳理,加深了對兩套架構(gòu)差異性的認(rèn)知和理解,尤其 wepy 上層語法和 Vue 的組件開發(fā)的代碼上的差異性?;趫F(tuán)隊對編譯的認(rèn)知,我們認(rèn)為借助 babel 等成熟編譯技術(shù)是有能力實現(xiàn)這個轉(zhuǎn)換的過程,另外,通過編譯技術(shù)會極大的提升整體的遷移的效率。
2.3 方案對比
通過團(tuán)隊對方案的深入討論和技術(shù)預(yù)研,最終大家達(dá)成一致使用編譯轉(zhuǎn)換的方式(方案三)來進(jìn)行本次的技術(shù)升級。最終,通過實現(xiàn) wepy 到 uniapp 的編譯轉(zhuǎn)換器,使原本 25人/天的工作量,6s 完成。
如下動圖所示:
三、架構(gòu)設(shè)計
3.1 wepy 和 uniapp 單文件組件轉(zhuǎn)換
通過對 wepy 和 uniapp 的學(xué)習(xí),充分了解兩者之間的差異性和相識點。wepy 的文件設(shè)計和 Vue 的單文件非常的相似,包含 template 和 script 和 style 的三部分組成。
如下圖所示,
所以我們將文件拆解為 script,template,style 樣式三個部分,通過 transpiler 分別轉(zhuǎn)換。同時這個過程主要是對 script 和 template 進(jìn)行轉(zhuǎn)換,樣式和 Vue 可以保持一致性最終借助 Vue 進(jìn)行轉(zhuǎn)換即可。
同時 wepy 還有自己的 runtime運(yùn)行時的依賴,為了確保項目對 wepy 做到最小化的依賴,方便后續(xù)完全和 wepy 的依賴進(jìn)行完全解耦,我們抽取了一個 wepy-adapter 模塊,將原先對于 wepy 的依賴轉(zhuǎn)換為對wepy-adapter 的依賴。
整體轉(zhuǎn)換設(shè)計,如下圖所示:
3.2 編譯器流水線構(gòu)建
如上圖所示,整個編譯過程就是一條流水線的架構(gòu)設(shè)計,在每個階段完成不同的任務(wù)。主要流程如下:
1. 項目資源分析
不同的項目依賴資源不同的處理流程,掃描項目中的源碼和資源文件進(jìn)行分類,等待后續(xù)的不同的流水線處理。
靜態(tài)資源文件(圖片,樣式文件等)不需要經(jīng)過當(dāng)中流水線的處理,直達(dá)目標(biāo) uniapp 項目的對應(yīng)的目錄。
3.2.2 AST抽象語法樹轉(zhuǎn)換
針對 wepy 的源文件(app,page,component等)對 script,template 等部分,通過 parse 轉(zhuǎn)換成相對應(yīng)的AST抽象語法樹,后續(xù)的代碼轉(zhuǎn)換都是基于對抽象語法樹的結(jié)構(gòu)改進(jìn)。
3. 代碼轉(zhuǎn)換實現(xiàn) - Transform code
根據(jù) wepy 和 uniapp 的 Vue 的代碼實現(xiàn)上的差異,通過對ast進(jìn)行轉(zhuǎn)換實現(xiàn)代碼的轉(zhuǎn)換。
4. 代碼生成 - code emitter
根據(jù)步驟三轉(zhuǎn)換之后最終的ast,進(jìn)行對應(yīng)的代碼生成。
四、項目搭建
整體項目結(jié)構(gòu)如下圖所示:
4.1 單倉庫的管理模式
使用 lerna 進(jìn)行單倉庫的模塊化管理,方便進(jìn)行模塊的拆分和本地模塊之間依賴引用。另外單倉庫的好處在于,和項目相關(guān)的信息都可以在一個倉庫中沉淀下來,如文檔,demo,issue 等。不過隨著 lerna 社區(qū)不再進(jìn)行維護(hù),后續(xù)會將 lerna 遷移到 pnpm 的 workspace 的方案進(jìn)行管理。
4.2 核心模塊
wepy-adapter - wepy運(yùn)行期以來的最小化的polyfill
wepy-chameleon-cli - 命令行工具模塊
wepy-chameleon-transpiler - 核心的編譯器模塊,按照one feature,one module方式組織
4.3 自動化任務(wù)構(gòu)建等
Makefile - *nix世界的標(biāo)準(zhǔn)方式
4.4 scripts 自動化管理
shipit.ts 模塊的自動發(fā)布等自動化能力
4.5 單元測試
采用Jest作為基礎(chǔ)的測試框架,使用typescript來作為測試用例的編寫。
使用@swc/jest作為ts的轉(zhuǎn)換器,提升ts的編譯速度。
現(xiàn)在社區(qū)的vitest直接提供了對ts的集成,借助vite帶來更快的速度,計劃遷移中。
五、核心設(shè)計實現(xiàn)
5.1 wepy template 模版轉(zhuǎn)換
5.1.1 差異性梳理
下面我們可以先來大致看一下wepy的模板語法和uniapp的模板語法的區(qū)別。
圖:wepy模板和uni-app模板
從上圖可以看出,wepy模板使用了原生微信小程序的wxml語法,并且在采用類似Vue的組件引入機(jī)制的同時,保留了wxml< import/ >、< include/ >標(biāo)簽的能力。同時為了和wxml中循環(huán)渲染dom節(jié)點的語法做區(qū)別,引入了新的< Repeat/ >標(biāo)簽來渲染引入的子組件,而uni-app則是完全使用Vue風(fēng)格的語法來進(jìn)行開發(fā)。
所以總結(jié)wepy和uni-app模板語法的主要區(qū)別有兩點:
wepy使用了一些特定的標(biāo)簽用來導(dǎo)入或者復(fù)用其他wxml文件例如< import >和< include >。
wxml使用了xml命名空間的方式來定義模板指令,并且對指令值的處理更像是使用模板引擎對特定格式的變量進(jìn)行替換。
下表列舉一些兩者模板指令的對應(yīng)轉(zhuǎn)換關(guān)系。
此外,還有一些指令的細(xì)節(jié)需要處理,例如在wepy中wx:key="id"指令會自動解析為wx:key="{{item.id}}",這里便不再贅述。
5.1.2 核心轉(zhuǎn)換設(shè)計
編譯器對template轉(zhuǎn)換主要就需要完成以下三個步驟:
- 處理wepy引入的特殊的標(biāo)簽例如。
- 將wxml中使用的指令、特殊標(biāo)簽等轉(zhuǎn)換為Vue模板的語法。
- 收集引入的組件信息傳遞給下游的wepy-page-transform模塊。
- wepy特殊標(biāo)簽轉(zhuǎn)換
首先我們會處理wepy模板中的特殊標(biāo)簽< import/ >、< include/ >,主要是將wxml的文件引入模式轉(zhuǎn)換成Vue模板的組件引入模式,同時還需要收集引入的wxml的文件地址和展示的模板名稱。由于< include/ >可以引入wxml文件中除了< template/ >和< wxs/ >的所有代碼,為了保證轉(zhuǎn)換后組件的復(fù)用性,我們將引入的xx.wxml文件拆成了xx.vue和xx-incl.vue兩個文件,使用< import/ >標(biāo)簽的會導(dǎo)入xx.vue,而使用< include/ >標(biāo)簽的會導(dǎo)入xx-incl.vue,轉(zhuǎn)換import的核心代碼實現(xiàn)如下:
transformImport() {
// 獲取所有import標(biāo)簽
const imports = this.$('import')
for (let i = 0; i < imports.length; i++) {
const node = imports.eq(i)
if (!node.is('import')) return
const importPath = node.attr('src')
// 收集引入的路徑信息
this.importPath.push(importPath)
// 將文件名統(tǒng)一轉(zhuǎn)換成短橫線風(fēng)格
let compName = TransformTemplate.toLine(
path.basename(importPath, path.extname(importPath))
)
let template = node.next('template')
while (template.is('template')) {
const next = template.next('template')
if (template.attr('is')) {
const children = template.children()
// 生成新的組件標(biāo)簽例如
//
// =>
const comp = this.$(`<${compName} />`)
.attr(template.attr())
.append(children)
comp.attr(TransformTemplate.toLine(this.compName), comp.attr('is'))
comp.removeAttr('is')
// 將當(dāng)前標(biāo)簽替換為新生成的組件標(biāo)簽
template.replaceWith(comp)
}
template = next
}
node.remove()
}
}具體的WXML文件拆分方案請看WXML轉(zhuǎn)換部分。
- wepy 屬性轉(zhuǎn)換
上文中已經(jīng)介紹了,wepy模板中的屬性使用了命名空間+模板字符串風(fēng)格的動態(tài)屬性,我們需要將他們轉(zhuǎn)換成Vue風(fēng)格的屬性。轉(zhuǎn)換需要操作模板中的節(jié)點及其屬性,這里我們使用了cheerio, 快速、靈活、類jQuery核心實現(xiàn),可以利用jQuery的語法非常方便的對模板字符串進(jìn)行處理。
核心代碼如下:
上述流程中一個分支中的轉(zhuǎn)換函數(shù)會處理相應(yīng)的wepy屬性,以保證后續(xù)可以很方便的對轉(zhuǎn)換模塊進(jìn)行完善和修改。由于屬性名稱轉(zhuǎn)換只是簡單的做一下相應(yīng)的映射,我們重點分析一下動態(tài)屬性值的轉(zhuǎn)換過程。
WXML中使用雙中括號來標(biāo)記動態(tài)屬性中的變量及WXS表達(dá)式,并且如果變量是WXS對象的話還可以省略對象的大括號例如
< view wx:for="{{list}}" > {{item}} < /view >、< template is="objectCombine" data="{{for: a, bar: b}}" >< /template >所以當(dāng)我們?nèi)〉诫p中括號中的值時會有以下兩種情況:
① 得到WXS的表達(dá)式;
② 得到一個沒有中括號包裹的WXS對象。此時我們可以先對表達(dá)式嘗試轉(zhuǎn)換,如果有報錯的話,給表達(dá)式包裹一層中括號再進(jìn)行轉(zhuǎn)換??紤]到WXS的語法類似于Javascript的子集,我們依然使用babel對其進(jìn)行解析并處理。
核心代碼實現(xiàn)如下:
/**
*
* @param value 需要轉(zhuǎn)換的屬性值
*/
private transformValue(value: string): string {
const exp = value.match(TransformTemplate.dbbraceRe)[1]
try {
let seq = false
traverse(parseSync(`(${exp})`), {
enter(path) {
// 由于WXS支持對象鍵值相等的縮寫{{a,b,c}},故此處需要額外處理
if (path.isSequenceExpression()) {
seq = true
}
},
})
if (!seq) {
return exp
}
return `{${exp}}`
} catch (e) {
return `{${exp}}`
}
}
到這里,我們已經(jīng)能夠處理wepy模板中絕大部分的動態(tài)屬性值的轉(zhuǎn)換。但是,上文也提及到了,wepy采用的是類似模板引擎的方式來處理動態(tài)屬性的,即WXML支持這種動態(tài)屬性< view id="item-{{index}}" >,如果這個 < view / >標(biāo)簽使用了wx:for指令的話,id屬性會被編譯成item-0、item-1... 這個問題我們也想了多種方案去解決,例如字符串拼接、正則處理等,但是都不能很好的覆蓋全部場景,總會有特殊場景的出現(xiàn)導(dǎo)致轉(zhuǎn)換失敗。
最終,我們還是想到了模板引擎,Javascript中也有類似于模板引擎的元素,那就是模板字符串。使用模板字符串,我們僅僅需要把WXML中用來標(biāo)記變量的雙括號{{}}轉(zhuǎn)換成Javascript中的${}即可。
5.2 Wepy App 轉(zhuǎn)換
5.2.1 差異性梳理
wepy 的 App 小程序?qū)嵗兄饕〕绦蛏芷诤瘮?shù)、config 配置對象、globalData 全局?jǐn)?shù)據(jù)對象,以及其他自定義方法與屬性。
核心代碼實現(xiàn)如下:
import wepy from 'wepy'
// 在 page 中,通過 this.$parent 來訪問 app 實例
export default class MyAPP extends wepy.app {
customData = {}
customFunction() {}
onLaunch() {}
onShow() {}
// 對應(yīng) app.json 文件
// build 編譯時會根據(jù) config 屬性自動生成 app.json 文件
config = {}
globalData = {}
}
uniapp的 App.vue 可以定義小程序生命周期方法,globalData全局?jǐn)?shù)據(jù)對象,以及一些自定義方法,核心代碼實現(xiàn)如下:
可以看到,wepy的page類也是通過繼承來實現(xiàn)的,頁面文件 page.wpy 中所聲明的頁面實例繼承自 wepy.page 類,該類的主要屬性介紹如下:
5.4.2 核心轉(zhuǎn)換設(shè)計
基于page的api特性以及實現(xiàn)方案,具體的轉(zhuǎn)換設(shè)計思路如下:
5.4.3 痛點難點
1.非阻塞異步與異步
在進(jìn)行批量pages轉(zhuǎn)換時,需要同時對pages.json進(jìn)行讀取、修改、再修改的操作,這就涉及到使用阻塞 IO/ 異步 IO來處理文件的讀寫,當(dāng)使用異步IO時,會發(fā)起多個進(jìn)程同時處理pages.json, 每個讀取完成后單獨(dú)處理對應(yīng)的內(nèi)容,數(shù)據(jù)不是串行修改,最終導(dǎo)致最終修改的內(nèi)容不符合預(yù)期,因此在遇到并行處配置文件時,需要使用阻塞式io來讀取文件,保障最終數(shù)據(jù)的唯一性,具體代碼如下:
// merge pageConfig to app config
const rawPagesJson = fs.readFileSync(path.join(dest, 'src/pages.json'))
// 數(shù)據(jù)操作
fs.writeFileSync(
path.join(dest, 'src', 'pages.json'),
prettJson(pagesJson)
)
2.復(fù)雜的事件機(jī)制
在轉(zhuǎn)換過程中,我們也碰到一個比較大的痛點:page.wepy 繼承至 wepy.page,wepy.page 代碼較復(fù)雜,需要將明確部分單獨(dú)抽離出來。例如說 events 中組件間數(shù)據(jù)傳遞:`$broadcast`、`$emit`、`$invoke`,`$broadcast`、`$invoke`需要熟悉其使用場景,轉(zhuǎn)換為 Vue 中公共方法。
5.5 Wepy WXML 轉(zhuǎn)換
template轉(zhuǎn)換章節(jié)中提到了wepy模板中可以直接引入wxml文件,但是uni-app使用的Vue模板不支持直接引入wxml,故我們需要將wxml文件處理為uniapp可以引入的Vue文件。我們先來看一下wepy中引入的wxml文件的大致結(jié)構(gòu)。
{{item.text1}}
{{item.text2}}
this is footer


咨詢
建站咨詢