久久精品中文字幕免费_91香蕉国产亚洲一区二区三区_国产精品巨作无遮拦_亚洲人成电影

    <center id="oy65s"><ol id="oy65s"></ol></center>

  • <menu id="oy65s"></menu>
    當(dāng)前位置:首頁(yè) > 足球資訊 > 正文內(nèi)容

    從wepy到uniapp變形記

    杏彩體育2年前 (2023-01-29)足球資訊33

    原標(biāo)題:從wepy到uniapp變形記

    作者:vivo 互聯(lián)網(wǎng)前端團(tuán)隊(duì)-

    Wan Anwen、Hu Feng、Feng Wei、Xie Tao

    進(jìn)入互聯(lián)網(wǎng)“下半場(chǎng)”,靠“人海戰(zhàn)術(shù)”的研發(fā)模式已經(jīng)不再具備競(jìng)爭(zhēng)力,如何通過(guò)技術(shù)升級(jí)提升研發(fā)效能?前端通過(guò)Babel等編譯技術(shù)發(fā)展實(shí)現(xiàn)了工程化體系升級(jí),如何進(jìn)一步通過(guò)編譯技術(shù)賦能前端開(kāi)發(fā)?或許我們 wepy 到uniapp 編譯的轉(zhuǎn)換實(shí)踐,能給你帶來(lái)啟發(fā)。

    一、 背景

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

    早期的小程序開(kāi)發(fā)工程體系還不夠健全,和現(xiàn)在的前端的工程體系相差較大,表現(xiàn)在對(duì)模塊化,組件化以及高級(jí)Java 語(yǔ)法特性的支撐上。所以團(tuán)隊(duì)在做技術(shù)選型時(shí),希望克服原生小程序工程體系上的不足,經(jīng)過(guò)對(duì)比最后選擇了騰訊出品的 wepy 作為整體的開(kāi)發(fā)框架。

    在項(xiàng)目的從0到1階段,wepy 確實(shí)幫助我們實(shí)現(xiàn)了快速的業(yè)務(wù)迭代,滿足線下門(mén)店導(dǎo)購(gòu)的需求。但隨著時(shí)間的推移,在技術(shù)上,社區(qū)逐步沉淀出以 uniapp 為代表的 Vue 棧體系和以 Taro 為代表的 React ??缍说捏w系,wepy 目前的社區(qū)活躍度比較低。另外隨著業(yè)務(wù)進(jìn)入穩(wěn)定階段,除少量的 wepy 小程序,H5 項(xiàng)目和新的小程序都是基于 Vue 和 uniapp 來(lái)構(gòu)建,團(tuán)隊(duì)也是希望統(tǒng)一技術(shù)棧,實(shí)現(xiàn)更好的跨端開(kāi)發(fā)能力,降低開(kāi)發(fā)和維護(hù)成本,提升研發(fā)效率。

    二、思考

    隨著團(tuán)隊(duì)決定將智能導(dǎo)購(gòu)小程序從 wepy 遷移到 uniapp 的架構(gòu)體系,我們就需要思考,如何進(jìn)行項(xiàng)目的平穩(wěn)的遷移,同時(shí)兼顧效率和質(zhì)量?通過(guò)對(duì)當(dāng)前的項(xiàng)目狀態(tài)和技術(shù)背景進(jìn)行分析,團(tuán)隊(duì)梳理出2個(gè)原則3種遷移思路。

    2.1 漸進(jìn)式遷移

    核心出發(fā)點(diǎn),保證項(xiàng)目的平穩(wěn)過(guò)渡,給團(tuán)隊(duì)更多的時(shí)間,在迭代中逐步的進(jìn)行架構(gòu)遷移。希望以此來(lái)降低遷移中的風(fēng)險(xiǎn)和不可控的點(diǎn)?;诖耍覀兯伎純蓚€(gè)方案:

    方案一 融合兩套架構(gòu)體系

    在目前的項(xiàng)目中引入和 uniapp 的項(xiàng)目體系,一個(gè)項(xiàng)目融合了 wepy 和 uniapp 的代碼工程化管理,逐步的將 wepy 的代碼改成 uniapp 的代碼,待遷移完成刪除 wepy 的目錄。這種方案實(shí)現(xiàn)起來(lái)不是很復(fù)雜,但是缺點(diǎn)是管理起來(lái)比較復(fù)雜,兩套工程化管理機(jī)制,底層的編譯機(jī)制,各種入口的配置文件等,管理起來(lái)比較麻煩。另外團(tuán)隊(duì)每個(gè)人都需要消化 wepy 到 uniapp 的領(lǐng)域知識(shí)遷移,不僅僅是項(xiàng)目的遷移也是知識(shí)體系的遷移。

    方案二 設(shè)計(jì) wepy-webpack-loader

    以 uniapp 為工程體系基礎(chǔ),核心思路是將現(xiàn)有 wepy 代碼融入到 uniapp 的體系中來(lái)。我們都知道 uniapp 的底層依賴于 Vue 的 cli 的技術(shù)體系,最底層通過(guò) webpack 實(shí)現(xiàn)對(duì) Vue 單組件文件和其他資源文件的 bundle。

    基于此,我們可以開(kāi)發(fā)一個(gè) wepy 的 webpack 的 loader,wepy-loader 類似于 vue-loader 的能力,通過(guò)該 loader 對(duì) wepy 文件進(jìn)行編譯打包,然后最終輸出小程序代碼。想法很簡(jiǎn)單,但我們想要實(shí)現(xiàn) wepy-loader工作量還是比較大的,需要對(duì) wepy 的底層編譯器進(jìn)一步進(jìn)行分析拆解,分析 wepy 的依賴關(guān)系,區(qū)分是組件編譯還是 page 編譯等,且 wepy 底層編譯器的代碼比較復(fù)雜,實(shí)現(xiàn)成本較高。

    2.2 整體性遷移

    構(gòu)建一個(gè)編譯器實(shí)現(xiàn) wepy 到 uniapp 的自動(dòng)代碼轉(zhuǎn)換

    通過(guò)對(duì) wepy 和 uniapp 整體技術(shù)方案的梳理,加深了對(duì)兩套架構(gòu)差異性的認(rèn)知和理解,尤其 wepy 上層語(yǔ)法和 Vue 的組件開(kāi)發(fā)的代碼上的差異性?;趫F(tuán)隊(duì)對(duì)編譯的認(rèn)知,我們認(rèn)為借助 babel 等成熟編譯技術(shù)是有能力實(shí)現(xiàn)這個(gè)轉(zhuǎn)換的過(guò)程,另外,通過(guò)編譯技術(shù)會(huì)極大的提升整體的遷移的效率。

    2.3 方案對(duì)比

    通過(guò)團(tuán)隊(duì)對(duì)方案的深入討論和技術(shù)預(yù)研,最終大家達(dá)成一致使用編譯轉(zhuǎn)換的方式(方案三)來(lái)進(jìn)行本次的技術(shù)升級(jí)。最終,通過(guò)實(shí)現(xiàn) wepy 到 uniapp 的編譯轉(zhuǎn)換器,使原本 25人/天的工作量,6s 完成。

    如下動(dòng)圖所示:

    三、架構(gòu)設(shè)計(jì)

    3.1 wepy 和 uniapp 單文件組件轉(zhuǎn)換

    通過(guò)對(duì) wepy 和 uniapp 的學(xué)習(xí),充分了解兩者之間的差異性和相識(shí)點(diǎn)。wepy 的文件設(shè)計(jì)和 Vue 的單文件非常的相似,包含 template 和 和 style 的三部分組成。

    如下圖所示,

    所以我們將文件拆解為 ,template,style 樣式三個(gè)部分,通過(guò) transpiler 分別轉(zhuǎn)換。同時(shí)這個(gè)過(guò)程主要是對(duì) 和 template 進(jìn)行轉(zhuǎn)換,樣式和 Vue 可以保持一致性最終借助 Vue 進(jìn)行轉(zhuǎn)換即可。

    同時(shí) wepy 還有自己的 runtime運(yùn)行時(shí)的依賴,為了確保項(xiàng)目對(duì) wepy 做到最小化的依賴,方便后續(xù)完全和 wepy 的依賴進(jìn)行完全解耦,我們抽取了一個(gè) wepy-adapter 模塊,將原先對(duì)于 wepy 的依賴轉(zhuǎn)換為對(duì)wepy-adapter 的依賴。

    整體轉(zhuǎn)換設(shè)計(jì),如下圖所示:

    3.2 編譯器流水線構(gòu)建

    如上圖所示,整個(gè)編譯過(guò)程就是一條流水線的架構(gòu)設(shè)計(jì),在每個(gè)階段完成不同的任務(wù)。主要流程如下:

    1. 項(xiàng)目資源分析

    不同的項(xiàng)目依賴資源不同的處理流程,掃描項(xiàng)目中的源碼和資源文件進(jìn)行分類,等待后續(xù)的不同的流水線處理。

    靜態(tài)資源文件(圖片,樣式文件等)不需要經(jīng)過(guò)當(dāng)中流水線的處理,直達(dá)目標(biāo) uniapp 項(xiàng)目的對(duì)應(yīng)的目錄。

    2. AST抽象語(yǔ)法樹(shù)轉(zhuǎn)換

    針對(duì) wepy 的源文件(app,page,component等)對(duì) ,template 等部分,通過(guò) parse 轉(zhuǎn)換成相對(duì)應(yīng)的AST抽象語(yǔ)法樹(shù),后續(xù)的代碼轉(zhuǎn)換都是基于對(duì)抽象語(yǔ)法樹(shù)的結(jié)構(gòu)改進(jìn)。

    3. 代碼轉(zhuǎn)換實(shí)現(xiàn) - Transform code

    根據(jù) wepy 和 uniapp 的 Vue 的代碼實(shí)現(xiàn)上的差異,通過(guò)對(duì)ast進(jìn)行轉(zhuǎn)換實(shí)現(xiàn)代碼的轉(zhuǎn)換。

    4. 代碼生成 - code emitter

    根據(jù)步驟三轉(zhuǎn)換之后最終的ast,進(jìn)行對(duì)應(yīng)的代碼生成。

    四、項(xiàng)目搭建

    整體項(xiàng)目結(jié)構(gòu)如下圖所示:

    4.1 單倉(cāng)庫(kù)的管理模式

    使用 lerna 進(jìn)行單倉(cāng)庫(kù)的模塊化管理,方便進(jìn)行模塊的拆分和本地模塊之間依賴引用。另外單倉(cāng)庫(kù)的好處在于,和項(xiàng)目相關(guān)的信息都可以在一個(gè)倉(cāng)庫(kù)中沉淀下來(lái),如文檔,demo,issue 等。不過(guò)隨著 lerna 社區(qū)不再進(jìn)行維護(hù),后續(xù)會(huì)將 lerna 遷移到 pnpm 的 workspace 的方案進(jìn)行管理。

    4.2 核心模塊

    wepy-adapter - wepy運(yùn)行期以來(lái)的最小化的polyfill wepy-chameleon-cli - 命令行工具模塊 wepy-chameleon-transpiler - 核心的編譯器模塊,按照one feature,one module方式組織

    4.3 自動(dòng)化任務(wù)構(gòu)建等

    Makefile - *nix世界的標(biāo)準(zhǔn)方式

    4.4 s 自動(dòng)化管理

    shipit.ts 模塊的自動(dòng)發(fā)布等自動(dòng)化能力

    4.5 單元測(cè)試

    采用Jest作為基礎(chǔ)的測(cè)試框架,使用type來(lái)作為測(cè)試用例的編寫(xiě)。 使用@swc/jest作為ts的轉(zhuǎn)換器,提升ts的編譯速度。 現(xiàn)在社區(qū)的vitest直接提供了對(duì)ts的集成,借助vite帶來(lái)更快的速度,計(jì)劃遷移中。

    五、核心設(shè)計(jì)實(shí)現(xiàn)

    5.1 wepy template 模版轉(zhuǎn)換

    5.1.1 差異性梳理

    下面我們可以先來(lái)大致看一下wepy的模板語(yǔ)法和uniapp的模板語(yǔ)法的區(qū)別。

    圖:wepy模板和uni-app模板

    從上圖可以看出,wepy模板使用了原生微信小程序的wxml語(yǔ)法,并且在采用類似Vue的組件引入機(jī)制的同時(shí),保留了wxml< import/ >、< include/ >標(biāo)簽的能力。同時(shí)為了和wxml中循環(huán)渲染dom節(jié)點(diǎn)的語(yǔ)法做區(qū)別,引入了新的< Repeat/ >標(biāo)簽來(lái)渲染引入的子組件,而uni-app則是完全使用Vue風(fēng)格的語(yǔ)法來(lái)進(jìn)行開(kāi)發(fā)。

    所以總結(jié)wepy和uni-app模板語(yǔ)法的主要區(qū)別有兩點(diǎn):

    wepy使用了一些特定的標(biāo)簽用來(lái)導(dǎo)入或者復(fù)用其他wxml文件例如< import >和< include >。 wxml使用了xml命名空間的方式來(lái)定義模板指令,并且對(duì)指令值的處理更像是使用模板引擎對(duì)特定格式的變量進(jìn)行替換。

    下表列舉一些兩者模板指令的對(duì)應(yīng)轉(zhuǎn)換關(guān)系。

    此外,還有一些指令的細(xì)節(jié)需要處理,例如在wepy中wx:key="id"指令會(huì)自動(dòng)解析為wx:key="{{item.id}}",這里便不再贅述。

    5.1.2 核心轉(zhuǎn)換設(shè)計(jì)

    編譯器對(duì)template轉(zhuǎn)換主要就需要完成以下三個(gè)步驟:

    處理wepy引入的特殊的標(biāo)簽例如。 將wxml中使用的指令、特殊標(biāo)簽等轉(zhuǎn)換為Vue模板的語(yǔ)法。 收集引入的組件信息傳遞給下游的wepy-page-transform模塊。 wepy特殊標(biāo)簽轉(zhuǎn)換

    首先我們會(huì)處理wepy模板中的特殊標(biāo)簽< import/ >、< include/ >,主要是將wxml的文件引入模式轉(zhuǎn)換成Vue模板的組件引入模式,同時(shí)還需要收集引入的wxml的文件地址和展示的模板名稱。由于< include/ >可以引入wxml文件中除了< template/ >和< wxs/ >的所有代碼,為了保證轉(zhuǎn)換后組件的復(fù)用性,我們將引入的xx.wxml文件拆成了xx.vue和xx-incl.vue兩個(gè)文件,使用< import/ >標(biāo)簽的會(huì)導(dǎo)入xx.vue,而使用< include/ >標(biāo)簽的會(huì)導(dǎo)入xx-incl.vue,轉(zhuǎn)換import的核心代碼實(shí)現(xiàn)如下:

    transformImport { // 獲取所有import標(biāo)簽 constimports = this.$(import) for(leti = 0; i < imports.length; i++) { constnode = imports.eq(i) if(!node.is(import)) return constimportPath = node.attr(src) // 收集引入的路徑信息 this.importPath.push(importPath) // 將文件名統(tǒng)一轉(zhuǎn)換成短橫線風(fēng)格 letcompName = TransformTemplate.toLine( path.basename(importPath, path.extname(importPath)) ) lettemplate = node.next(template) while(template.is(template)) { constnext = template.next(template) if(template.attr(is)) { constchildren = template.children // 生成新的組件標(biāo)簽例如 // <import src="components/list.wxml" /> // <template is="subList" /> => <list is="subList" /> constcomp = 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文件拆分方案請(qǐng)看WXML轉(zhuǎn)換部分。

    wepy 屬性轉(zhuǎn)換

    上文中已經(jīng)介紹了,wepy模板中的屬性使用了命名空間+模板字符串風(fēng)格的動(dòng)態(tài)屬性,我們需要將他們轉(zhuǎn)換成Vue風(fēng)格的屬性。轉(zhuǎn)換需要操作模板中的節(jié)點(diǎn)及其屬性,這里我們使用了cheerio, 快速、靈活、類jQuery核心實(shí)現(xiàn),可以利用jQuery的語(yǔ)法非常方便的對(duì)模板字符串進(jìn)行處理。

    上述流程中一個(gè)分支中的轉(zhuǎn)換函數(shù)會(huì)處理相應(yīng)的wepy屬性,以保證后續(xù)可以很方便的對(duì)轉(zhuǎn)換模塊進(jìn)行完善和修改。由于屬性名稱轉(zhuǎn)換只是簡(jiǎn)單的做一下相應(yīng)的映射,我們重點(diǎn)分析一下動(dòng)態(tài)屬性值的轉(zhuǎn)換過(guò)程。

    WXML中使用雙中括號(hào)來(lái)標(biāo)記動(dòng)態(tài)屬性中的變量及WXS表達(dá)式,并且如果變量是WXS對(duì)象的話還可以省略對(duì)象的大括號(hào)例如

    < viewwx:for="{{list}}">{{item}} < /view>、< templateis="objectCombine"data="{{for: a, bar: b}}">< /template>

    所以當(dāng)我們?nèi)〉诫p中括號(hào)中的值時(shí)會(huì)有以下兩種情況:

    ① 得到WXS的表達(dá)式;

    ② 得到一個(gè)沒(méi)有中括號(hào)包裹的WXS對(duì)象。此時(shí)我們可以先對(duì)表達(dá)式嘗試轉(zhuǎn)換,如果有報(bào)錯(cuò)的話,給表達(dá)式包裹一層中括號(hào)再進(jìn)行轉(zhuǎn)換??紤]到WXS的語(yǔ)法類似于Java的子集,我們依然使用babel對(duì)其進(jìn)行解析并處理。

    核心代碼實(shí)現(xiàn)如下:

    /** * * @param value 需要轉(zhuǎn)換的屬性值 */ privatetransformValue(value: string): string{ constexp = value.match(TransformTemplate.dbbraceRe)[1] try{ letseq = false traverse(parseSync(`(${exp})`), { enter(path) { // 由于WXS支持對(duì)象鍵值相等的縮寫(xiě){{a,b,c}},故此處需要額外處理 if(path.isSequenceExpression) { seq = true } }, }) if(!seq) { returnexp } return`{${exp}}` } catch(e) { return`{${exp}}` } }

    到這里,我們已經(jīng)能夠處理wepy模板中絕大部分的動(dòng)態(tài)屬性值的轉(zhuǎn)換。但是,上文也提及到了,wepy采用的是類似模板引擎的方式來(lái)處理動(dòng)態(tài)屬性的,即WXML支持這種動(dòng)態(tài)屬性< view id="item-{{index}}" >,如果這個(gè) < view / >標(biāo)簽使用了wx:for指令的話,id屬性會(huì)被編譯成item-0、item-1... 這個(gè)問(wèn)題我們也想了多種方案去解決,例如字符串拼接、正則處理等,但是都不能很好的覆蓋全部場(chǎng)景,總會(huì)有特殊場(chǎng)景的出現(xiàn)導(dǎo)致轉(zhuǎn)換失敗。

    最終,我們還是想到了模板引擎,Java中也有類似于模板引擎的元素,那就是模板字符串。使用模板字符串,我們僅僅需要把WXML中用來(lái)標(biāo)記變量的雙括號(hào){{}}轉(zhuǎn)換成Java中的${}即可。

    5.2 Wepy App 轉(zhuǎn)換

    5.2.1 差異性梳理

    wepy 的 App 小程序?qū)嵗兄饕〕绦蛏芷诤瘮?shù)、config 配置對(duì)象、globalData 全局?jǐn)?shù)據(jù)對(duì)象,以及其他自定義方法與屬性。

    核心代碼實(shí)現(xiàn)如下:

    importwepy fromwepy // 在 page 中,通過(guò) this.$parent 來(lái)訪問(wèn) app 實(shí)例 exportdefaultclassMyAPPextendswepy.app{ customData = {} customFunction {} onLaunch {} onShow {} // 對(duì)應(yīng) app.json 文件 // build 編譯時(shí)會(huì)根據(jù) config 屬性自動(dòng)生成 app.json 文件 config = {} globalData = {} }

    uniapp的 App.vue 可以定義小程序生命周期方法,globalData全局?jǐn)?shù)據(jù)對(duì)象,以及一些自定義方法,核心代碼實(shí)現(xiàn)如下:

    <> exportdefault{ globalData: { text: text } onLaunch: function() { console.log(App Launch,app啟動(dòng)) }, onShow: function() { console.log(App Show,app展現(xiàn)在前臺(tái)) }, onHide: function() { console.log(App Hide,app不再展現(xiàn)在前臺(tái)) }, methods: { // ..... } } <>

    5.2.2 核心轉(zhuǎn)換設(shè)計(jì)

    如圖, 核心轉(zhuǎn)換設(shè)計(jì)流程:

    對(duì) app.py 進(jìn)行 parse,拆分出和style部分,對(duì)部分使用babel進(jìn)行parse生成AST。 通過(guò)對(duì) AST 分析出,小程序的生命周期方法,globalData全局?jǐn)?shù)據(jù),自定義方法等。 對(duì)于AST進(jìn)行uniapp轉(zhuǎn)換,生命周期方法和全局?jǐn)?shù)據(jù)轉(zhuǎn)成對(duì)象的方法和屬性,對(duì)自定義方法轉(zhuǎn)換到method內(nèi)。 其中對(duì) globalData 的訪問(wèn),要進(jìn)行替換通過(guò) getApp進(jìn)行訪問(wèn)。 抽取 ast 中的 config 字段,輸出到 app.json 配置文件。 抽取 wepy.config.js 中的 config 字段,傳入 wepy 的 app 實(shí)例。

    核心代碼實(shí)現(xiàn):

    letAPP_EVENT = [onLaunch, onShow, onHide, , onPageNotFound] //.... // 實(shí)現(xiàn)wepy app到uniapp App.vue的轉(zhuǎn)換 t.program([ ...body.filter((node: t.Node) =>!t.isExportDeclaration(node)), // 插入appClass ...appClass, ...body .filter((node: t.Node) =>t.isExportDeclaration(node)) .map((node: object) =>{ // 對(duì)導(dǎo)出的app進(jìn)行處理 if(t.isExportDeclaration(node)) { // 提前config屬性 const{ appEvents, methods, props } = this.clzProperty // 重新導(dǎo)出vue style的對(duì)象 returnt.exportDefaultDeclaration( t.objectExpression([ // mixins ...mixins, // props ...Object.keys(props) .filter((elem) =>elem !== config) .map((elem) => this.transformClassPropertyToObjectProperty(props[elem]) ), // app events ...appEvents.map((elem) => this.transformClassMethodToObjectMethod(elem) ), // methods t.objectProperty( t.identifier(methods), t.objectExpression([ ...methods.map((elem) => this.transformClassMethodToObjectMethod(elem) ), ]) ), ]) ) } returnnode }), ]) // .....

    5.2.3 痛點(diǎn)難點(diǎn)

    在運(yùn)行期,app.wpy 會(huì)繼承 wepy.App 類,這樣就會(huì)在運(yùn)行期和 wepy.App 產(chǎn)生依賴關(guān)系,怎么最小化弱化這種關(guān)系。抽取wepy的最小化以來(lái)的polyfill,隨著業(yè)務(wù)中代碼剔除對(duì)wepy的api調(diào)用,最終去除對(duì)polyfill的依賴。

    5.3 wepy component 轉(zhuǎn)換

    對(duì)于wepy component 的轉(zhuǎn)換主要可以細(xì)化到對(duì) component 中 template、、style 三部分代碼塊的轉(zhuǎn)換。

    其中, style 部分由于已經(jīng)兼容 Vue 的規(guī)范,所以我們無(wú)需做額外處理。而 template 模塊主要是需要對(duì) wepy template 中特殊的標(biāo)簽、屬性、事件等內(nèi)容進(jìn)行處理,轉(zhuǎn)化為適配 uni的template,上文做了詳細(xì)的說(shuō)明。

    我們只需要專注于處理 模塊的代碼轉(zhuǎn)換即可。從架構(gòu)設(shè)計(jì)的思路來(lái)看,component 的轉(zhuǎn)換主要是做以下兩件事:

    編譯期可確定代碼塊的轉(zhuǎn)換。 運(yùn)行期動(dòng)態(tài)注入代碼的兼容。

    wepy-component-transform 就是基于以上這兩個(gè)標(biāo)準(zhǔn)設(shè)計(jì)出來(lái)的實(shí)現(xiàn)轉(zhuǎn)換邏輯的模塊。

    5.3.1 差異性梳理

    首先先解釋一下什么是“編譯期可確定代碼塊”,我們來(lái)看一個(gè) wepy 和 Vue 語(yǔ)法對(duì)比示例:

    從直觀上來(lái)說(shuō),這個(gè) 的模板的語(yǔ)法大致和 Vue 語(yǔ)法類似,這意味著我們解析出來(lái)的 AST 結(jié)構(gòu)和 Vue 文件對(duì)應(yīng)的 AST 結(jié)構(gòu)上類似,基于這一點(diǎn)來(lái)看編譯轉(zhuǎn)換的工作量大致有底了。

    從細(xì)節(jié)來(lái)看, wpy 文件 模塊中的 API 語(yǔ)法和 Vue 中有聲明及使用上的不同,其中包含:

    wepy 自身的包依賴注入及運(yùn)行時(shí)依賴 props/data/methods 聲明方式不同 生命周期鉤子不同 事件發(fā)布/訂閱的注冊(cè)和監(jiān)聽(tīng)機(jī)制不同。 ....等等

    為了確定這個(gè)第5點(diǎn)等等還存在哪些使用場(chǎng)景,我們需要對(duì) wepy 自身的邏輯和玩法有一個(gè)詳盡的了解和熟悉,通過(guò)在團(tuán)隊(duì)內(nèi)組織的 wepy 源碼走讀,再結(jié)合wepy 實(shí)際生產(chǎn)項(xiàng)目中的代碼相互印鑒,我們最終才將 wepy 語(yǔ)法邏輯與 uni-app Vue 語(yǔ)法邏輯的異同梳理清楚。

    5.3.2 核心轉(zhuǎn)換設(shè)計(jì)

    我們簡(jiǎn)單梳理一下 wepy-component-transform 這個(gè)模塊的結(jié)構(gòu),可以分為以下三個(gè)部分:

    預(yù)處理 wepy component 代碼 AST 節(jié)點(diǎn)部分 構(gòu)建 Vue AST 通過(guò) generate 吐出代碼

    1. 預(yù)處理 AST

    基于前文轉(zhuǎn)換設(shè)計(jì)這一節(jié)我們知道, wepy 變色龍的轉(zhuǎn)換器中對(duì)代碼的 AST 解析主要依賴 babel AST 三板斧(traverse、types、generate)來(lái)實(shí)現(xiàn),通過(guò)分析各個(gè)差異點(diǎn)代碼語(yǔ)句轉(zhuǎn)換后的 AST 節(jié)點(diǎn),就可以通過(guò) traverse 中的鉤子來(lái)進(jìn)行節(jié)點(diǎn)的前置處理,這里安利一下 https://astexplorer.net/,我們可以通過(guò)它快速分析代碼塊 AST 節(jié)點(diǎn)、模擬場(chǎng)景及驗(yàn)證轉(zhuǎn)換邏輯:

    預(yù)處理 AST,目的是提前將 wepy 源碼中的代碼塊解析為 AST Node 節(jié)點(diǎn)后,按語(yǔ)法進(jìn)行歸集到預(yù)置的 clzProperty 對(duì)象中,其中:

    props 對(duì)象用來(lái)盛放 ClassProperty 語(yǔ)法的 ast 節(jié)點(diǎn) notCompatibleMethods 數(shù)組用來(lái)盛放非生命周期函數(shù)白名單內(nèi)的函數(shù) AST 節(jié)點(diǎn)。 appEvents 數(shù)組用來(lái)盛放生命周期函數(shù)白名單內(nèi)的函數(shù) AST 節(jié)點(diǎn)。 listenEvents 數(shù)組用來(lái)盛放 發(fā)布/訂閱事件注冊(cè)的函數(shù) AST 節(jié)點(diǎn)。

    核心代碼實(shí)現(xiàn)如下所示:

    import{ NodePath, traverse, types } from@babel/core this.clzProperty = { props: {}, notCompatibleMethods: [], appEvents: [], listenEvents: [] } traverse { ClassProperty: (path) =>{ constname = path.node.key.name this.clzPropertyprops[name] = path.node }, ClassMethod: (path) =>{ constmethodName = path.node.key.name // 判斷是否存在于生命周期白名單內(nèi) constisCompEvent = TOTAL_EVENT.includes(methodName) if(isCompEvent) { this.clzProperty.appEvents.push(path.node) } else{ this.clzProperty.notCompatibleMethods.push(path.node) } }, ObjectMethod: (path: any) =>{ if(path.parentPath?.container?.key?.name === events) { this.clzProperty.listenEvents.push(path.node) } } }

    這里要注意一點(diǎn),由于對(duì) wepy 來(lái)說(shuō),實(shí)際上 page 也屬于 component 的一種實(shí)現(xiàn),所以兩者的 event 會(huì)有一定的重合,而且由于 wepy 中生命周期和 Vue 生命周期的差異性,我們需要對(duì)如 attached、detached、ready 等鉤子做一些 hack。

    2. 構(gòu)建 Vue AST

    buildCompVueAst 函數(shù)即為 構(gòu)建 Vue AST 部分。從直觀上來(lái)看,這個(gè)函數(shù)只做了一件事,即用 types.program 重新生成一個(gè) AST 節(jié)點(diǎn)結(jié)構(gòu),然后將原有的 wepy 語(yǔ)法轉(zhuǎn)換為 vue 語(yǔ)法。但是實(shí)際上我們還需要處理許多額外的兼容邏輯,簡(jiǎn)單羅列一下:

    created 重疊問(wèn)題 methods 中函數(shù)的收集 events 中函數(shù)的調(diào)用處理

    created 重疊問(wèn)題主要是為了解決 created/attached//onReady 這4個(gè)生命周期函數(shù)都會(huì)轉(zhuǎn)換為 created 導(dǎo)致的多次重復(fù)聲明問(wèn)題。我們需要針對(duì)若存在 created 重疊問(wèn)題時(shí),將其余鉤子中的代碼塊取出并 push 到第一個(gè) created 鉤子函數(shù)內(nèi)部。代碼示例如下:

    constbody = this.ast.program.body const{ appEvents, notCompatibleMethods, props, listenEvents } = this.clzProperty // 處理多個(gè) created 生命周期重疊問(wèn)題 constcreateIndexs: number[] = [] constsameList = [created, attached, , onReady] appEvents.forEach((node, index) =>{ constname: string= node.key.name if(sameList.includes(name)) { createIndexs.push(index) } }) if(createIndexs.length > 1) { // 取出源節(jié)點(diǎn)內(nèi)代碼塊 constoriginIndex = createIndexs[0] constoriginNode = appEvents[originIndex] constoriginBodyNode = originNode.body.body // 留下的剩余節(jié)點(diǎn)需要取出其代碼塊并塞入源節(jié)點(diǎn)中 // 塞入完成后刪除剩余節(jié)點(diǎn) createIndexs.splice(0, 1) createIndexs.forEach((index) =>{ consttargetNode = appEvents[index] consttargetBodyNode = targetNode.body.body // 將源節(jié)點(diǎn)內(nèi)代碼塊塞入目標(biāo)節(jié)點(diǎn)中 originBodyNode.push(...targetBodyNode) // 刪除源節(jié)點(diǎn) appEvents.splice(index, 1) }) }

    由于 wepy 中非 methods 中函數(shù)的特殊性,所以我們需要在轉(zhuǎn)換時(shí)將獨(dú)立聲明的函數(shù)、events 中的函數(shù)都抽離出來(lái)再 push 到 methods 中,偽代碼邏輯如下所示:

    buildCompVueAst { constbody = this.ast.program.body returnt.program([ ...body.map((node) =>{ returnt.exportDefaultDeclaration( t.objectExpression([ ...Object.keys(props) .map((elem) =>{ if(elem === methods) { constnode = props[elem] // 1.events 內(nèi)函數(shù)插入 methods 中 // 2.與生命周期平級(jí)的函數(shù)抽離出來(lái)插入 methods 中 node.value.properties.push( ...listenEvents, ...notCompatibleMethods ) } returnprops[elem] }) ]) ) }) ]) }

    events 中函數(shù)的調(diào)用處理主要是為了抹平 wepy 中發(fā)布訂閱事件調(diào)用和 Vue 調(diào)用的差異性。在 wepy 中,事件的注冊(cè)通過(guò)在 events 中聲明函數(shù),事件的調(diào)用通過(guò) this.$emit 來(lái)觸發(fā)。而 vue 中我們采用的是 EventBus 方案來(lái)兼容 wepy 中的寫(xiě)法,即手動(dòng)為 events 中的函數(shù)創(chuàng)建 this.$on 形式的調(diào)用,并將其代碼塊按順序塞入 created 中來(lái)初始化。

    首先我們要判斷文件中是否已有 created 函數(shù),若存在,則獲取其對(duì)應(yīng)的代碼塊并調(diào)用 forEachListenEvents 函數(shù)將 events 中的監(jiān)聽(tīng)都 push 進(jìn)去。

    若不存在,則初始化一個(gè)空的 created 容器,并調(diào)用 forEachListenEvents 函數(shù)。核心代碼實(shí)現(xiàn)如下所示:

    buildCompVueAst { constobp = [] astypes.ObjectMethod[] // 獲取class屬性和方法 constbody = node.declaration.body.body consttargetNodeArray = body.filter(child=> child.key.name === created ) if(targetNodeArray.length > 0) { letcreatedNode = targetNodeArray[0] this.forEachListenEvents(createdNode) } else{ consttargetNode = t.objectMethod( method, t.identifier(created), [], t.blockStatement([]) ) this.forEachListenEvents(targetNode) if(targetNode.body && targetNode.body.body.length > 0) { obp.push(targetNode) } } returnobp }

    forEachListenEvents 函數(shù)主要是通過(guò) wepy 中 聲明的 events 事件名和入?yún)ⅲ柚?babel types 手動(dòng)創(chuàng)建對(duì)應(yīng)的 AST Node,最終生成對(duì)應(yīng)的形如 this.$eventBus.$on("canceldeposit", this.canceldeposit) 形式的監(jiān)聽(tīng),其中,this.canceldeposit 為原有 events 中的事件被移入 methods 后的函數(shù),相關(guān)偽代碼實(shí)現(xiàn)如下所示:

    // 根據(jù) events 中的 methods 構(gòu)建事件監(jiān)聽(tīng)的調(diào)用 // 并塞入 created 中 forEachListenEvents(targetNode: types.ObjectMethod) { this.clzProperty.listenEvents.forEach((item) =>{ constmethodsNode: any= item // 形如 this.$on(test, =>{}) if(methodsNode?.key?.name) { // 創(chuàng)建 this 表達(dá)式 constthisEx = t.thisExpression // 創(chuàng)建 $on 表達(dá)式 constide = t.identifier($eventBus.$on) // 合并 this.$on 表達(dá)式 constom = t.memberExpression(thisEx, ide) // 創(chuàng)建事件名稱參數(shù)節(jié)點(diǎn) consteventNameIde = t.stringLiteral( methodsNode.key.name.toString.trim ) // 獲取方法體內(nèi)代碼內(nèi)容節(jié)點(diǎn) constmeNode = t.memberExpression( t.thisExpression, t.identifier(methodsNode.key.name.toString.trim) ) constceNode = t.callExpression(om, [eventNameIde, meNode]) constesNode = t.expressionStatement(ceNode) // 將合成后的代碼插入到 created 中 targetNode.body.body.push(esNode) } }) }

    3. emitter vue 代碼生成

    構(gòu)建完 Vue AST 之后,我們可以調(diào)用 generate 函數(shù)生成源碼字符串:

    transform { constast = this.buildCompVueAst constcompVue = this.genCode(ast) return{ compVue, wxs: this.buildWxs } }

    5.4 Wepy page 轉(zhuǎn)換

    5.4.1 差異性梳理

    上面的章節(jié)已經(jīng)給大家分析了template、component的代碼轉(zhuǎn)換邏輯,這一節(jié)主要帶大家一起看下如何轉(zhuǎn)換page文件。page轉(zhuǎn)換的邏輯即如何實(shí)現(xiàn)wepy 的 page.wpy 模塊轉(zhuǎn)換為 uniapp 的 page.vue 模塊。

    首先我們來(lái)看下wepy 的 page 小程序?qū)嵗?/p> <> importwepy fromwepy; importCounter from../components/counter; exportdefaultclassPageextendswepy.page{ config = {}; components = {counter1: Counter}; data = {}; methods = {}; events = {}; {}; // Other properties } </> <templatelang="wxml"> <view> </view> <counter1></counter1> </template> <stylelang="less"> /** less **/ </style>

    可以看到,wepy的page類也是通過(guò)繼承來(lái)實(shí)現(xiàn)的,頁(yè)面文件 page.wpy 中所聲明的頁(yè)面實(shí)例繼承自 wepy.page 類,該類的主要屬性介紹如下:

    5.4.2 核心轉(zhuǎn)換設(shè)計(jì)

    基于page的api特性以及實(shí)現(xiàn)方案,具體的轉(zhuǎn)換設(shè)計(jì)思路如下:

    5.4.3 痛點(diǎn)難點(diǎn)

    1.非阻塞異步與異步

    在進(jìn)行批量pages轉(zhuǎn)換時(shí),需要同時(shí)對(duì)pages.json進(jìn)行讀取、修改、再修改的操作,這就涉及到使用阻塞 IO/ 異步 IO來(lái)處理文件的讀寫(xiě),當(dāng)使用異步IO時(shí),會(huì)發(fā)起多個(gè)進(jìn)程同時(shí)處理pages.json, 每個(gè)讀取完成后單獨(dú)處理對(duì)應(yīng)的內(nèi)容,數(shù)據(jù)不是串行修改,最終導(dǎo)致最終修改的內(nèi)容不符合預(yù)期,因此在遇到并行處配置文件時(shí),需要使用阻塞式io來(lái)讀取文件,保障最終數(shù)據(jù)的唯一性,具體代碼如下:

    // merge pageConfig to app config constrawPagesJson = 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)換過(guò)程中,我們也碰到一個(gè)比較大的痛點(diǎn):page.wepy 繼承至 wepy.page,wepy.page 代碼較復(fù)雜,需要將明確部分單獨(dú)抽離出來(lái)。例如說(shuō) events 中組件間數(shù)據(jù)傳遞:`$broadcast`、`$emit`、`$invoke`,`$broadcast`、`$invoke`需要熟悉其使用場(chǎng)景,轉(zhuǎn)換為 Vue 中公共方法。

    5.5 Wepy WXML 轉(zhuǎn)換

    template轉(zhuǎn)換章節(jié)中提到了wepy模板中可以直接引入wxml文件,但是uni-app使用的Vue模板不支持直接引入wxml,故我們需要將wxml文件處理為uniapp可以引入的Vue文件。我們先來(lái)看一下wepy中引入的wxml文件的大致結(jié)構(gòu)。

    <templatename="foo"> <viewclass="foo-content"> <textclass="text1">{{item.text1}}</text> <imageclass="pic"src="{{pic.url}}"mode="aspectFill"></image> </view> </template> <templatename="bar"> <viewclass="bar-content"> <imageclass="bar"src="{{pic.url}}"mode="aspectFill"></image> <textclass="text2">{{item.text2}}</text> </view> </template> <viewclass="footer"> this is footer </view> <!-- index.wepy --> <!-- 引入文件 --> <importsrc="somePath/fooBar.wxml"/> <!-- 確定展示的template及傳入屬性 --> <is="foo"data="{{item, pic}}"/> <!-- or, 此時(shí)僅會(huì)展示<template/>以外的內(nèi)容即footer --> <includesrc="somePath/fooBar.wxml">

    5.5.1 差異性梳理

    從上面的代碼可以看出,一個(gè)WXML文件中支持多個(gè)不同name屬性的< template/ >標(biāo)簽,并且支持通過(guò)在引入設(shè)置data來(lái)傳入屬性。從上面的示例模板中我們可以分析出,除了需要將wepy使用的WXML語(yǔ)法轉(zhuǎn)換成vue模板語(yǔ)法外(這里的轉(zhuǎn)換交給了template模塊來(lái)處理),我們還需要處理以下的問(wèn)題。

    確定引入組件時(shí)的傳參格式 確定組件中傳入對(duì)象的屬性有哪些 處理< import/ >和< include/ >引入的文件時(shí)的情況

    5.5.2 核心轉(zhuǎn)換設(shè)計(jì)

    1. 確定引入組件時(shí)的傳入屬性方式

    首先需要將wepy組件引入形式改成Vue的組件引入方式。以上面的代碼為例,即將< import/ > 、< / >對(duì)的引入形式改寫(xiě)成< component-name / > 引入方式。我們會(huì)在轉(zhuǎn)換開(kāi)始前對(duì)代碼進(jìn)行掃描,收集模板中的引入文件信息,傳遞給wepy-page-transform模塊處理,在轉(zhuǎn)換后的Vue組件的< / >中進(jìn)行引入。并且將< is="foo" data="{{item, pic}}" / > 轉(zhuǎn)換為< FooBar is="foo" :data=(待定) / > 。這里就需要確定屬性傳遞的方式。

    從上面的代碼中可以看到,在WXML文件的< template/ >會(huì)自動(dòng)使用傳入的data屬性作為隱式的命名空間,從而不需要使用data.item來(lái)獲取item屬性。這里很自然的就會(huì)想到原來(lái)的< is="foo" data="{{item, pic}}" / >可以轉(zhuǎn)換成< FooBar compName="foo" :key1="val1" :key2="val2" ... / >。

    其中,key1,val1,key2,val2等為原data屬性對(duì)象中的鍵值對(duì),compName用來(lái)指定展示的部分。這樣處理的好處是,引入的WXML文件中使用相應(yīng)的傳入的屬性就不需要做額外的修改,并且比較符合我們一般引入Vue組件時(shí)傳入屬性的方式。

    雖然這種方案可以較少的改動(dòng)WXML文件中的模板,但是由于傳入的對(duì)象可能會(huì)在運(yùn)行期間進(jìn)行修改,我們?cè)诰幾g期間比較難以確定傳入的data對(duì)象中的鍵值對(duì)??紤]到實(shí)現(xiàn)的時(shí)間成本及難易程度,我們沒(méi)有選擇這種方案。

    目前我們所采用的方案是不去改變?cè)械膶傩詡魅敕绞?,即將組件引入標(biāo)簽轉(zhuǎn)換為< FooBar compName="foo" :data="{item, pic}" / >。從而省去分析傳入對(duì)象在運(yùn)行時(shí)的變動(dòng)。這里就引出了第二個(gè)問(wèn)題,如何確定組件中傳入的參數(shù)有哪些。

    2. 確定組件中的傳入的對(duì)象屬性

    由于Vue的模板中不會(huì)自動(dòng)使用傳入的對(duì)象作為命名空間,我們需要手動(dòng)的找到當(dāng)前待轉(zhuǎn)換的模板中所使用到的所有的變量。相應(yīng)的代碼如下:

    searchVars { constself = this constdomList = this.$(template *) // 獲取wxml文件中template節(jié)點(diǎn)下的所有text節(jié)點(diǎn) consttext = domList.text constdbbraceRe = newRegExp(TransformTemplate.dbbraceRe, g) letivar // 拿到所有被{{}}包裹的動(dòng)態(tài)表達(dá)式 while((ivar = dbbraceRe.exec(text))) { addVar(ivar[1]) } // 遍歷所有節(jié)點(diǎn)的屬性,獲取所有的動(dòng)態(tài)屬性 for(leti = 0; i < domList.length; i++) { constdom = domList.eq(i) constattrs = Object.keys(dom.attr) for(letattr ofattrs) { constvalue = dom.attr(attr) if(!TransformTemplate.dbbraceRe.test(value)) continue constexp = value.match(TransformTemplate.dbbraceRe)[1] try{ addVar(exp) } catch(e) { addVar(`{${exp}}`) } } } functionaddVar(exp: string) { traverse(parseSync(`(${exp})`), { // 利用babel分析表達(dá)式中的所有變量 Identifier(path) { if( path.parentPath.isMemberExpression && !path.parentPath.node.computed && path.parentPath.node.property === path.node ) return self.vars.add(path.node.name) // 收集變量 }, }) } }

    收集到所有的變量信息后,模板中的所有變量前面需要加上傳入的對(duì)象名稱,例如item.hp_title需要轉(zhuǎn)換成data.item.hp_title??紤]到模板的簡(jiǎn)潔性和后續(xù)的易維護(hù)性,我們把轉(zhuǎn)換統(tǒng)一放到< / >的computed字段中統(tǒng)一處理即可:

    <template> <!--...--> </template> <> exportdefault{ props: [data, compName], computed: { item { returndata.item }, pic { returndata.pic } } } </>

    3.處理 < import/ >和< include/ >兩種引入方式

    wepy模板有兩種引入組件的方式,一種是使用< import/ >< / >標(biāo)簽對(duì)進(jìn)行引入,還有一種是使用< include/ > 進(jìn)行引入,< include/ > 會(huì)引入WXML文件中除了< template/ >和< wxs/ >的其他標(biāo)簽。這里的處理方式就比較簡(jiǎn)單,我們把< include/ > 會(huì)引入的部分單獨(dú)抽取出來(lái),生成TItem-incl.vue文件,這樣即保證了生成代碼的可復(fù)用性,也降低< import/ >標(biāo)簽引入的部分生成的TItem.vue文件中的邏輯復(fù)雜度。生成的兩個(gè)文件的結(jié)構(gòu)如下:

    <!--TItem.vue--> <template> <view> <templatev-if="compName == foo"> <viewclass="foo"> <!--...--> </view> </template> <templatev-if="compName == bar"> <viewclass="bar"> <!--...--> </view> </template> </view> </template> <> exportdefault{ props: [compName, data], computed: { item { returnthis.data.item }, pic { returnthis.data.pic } } } </> <!--TItem-incl.vue--> <template> <view> <viewclass="footer"> this is footer </view> </view> </template>

    六、階段性成果

    截止到目前,司內(nèi)的企微導(dǎo)購(gòu)小程序項(xiàng)目通過(guò)接入變色龍編譯器已經(jīng)順利的從 wepy 遷移到了 uniApp 架構(gòu),原本預(yù)計(jì)需要 25人/天 的遷移工作量在使用了編譯器轉(zhuǎn)換后縮短到了 10s。這不僅僅只是提高了遷移的效率,也降低了遷移中的知識(shí)遷移成本,給后續(xù)業(yè)務(wù)上的快速迭代奠定的扎實(shí)的基礎(chǔ)。

    遷移后的企微導(dǎo)購(gòu)小程序項(xiàng)目經(jīng)測(cè)試階段驗(yàn)證業(yè)務(wù)功能 0 bug,目前已經(jīng)順利上線。后續(xù)我們也會(huì)持續(xù)收集其他類似的業(yè)務(wù)訴求,幫助業(yè)務(wù)兄弟們低成本完成遷移。

    七、總結(jié)

    研發(fā)能效的提升是個(gè)永恒的話題,此次我們從編譯這個(gè)角度出發(fā),和大家分享了從wepy到uniapp的架構(gòu)升級(jí)探索的過(guò)程,通過(guò)構(gòu)建代碼轉(zhuǎn)換的編譯器來(lái)提升整體的架構(gòu)升級(jí)效率,通過(guò)編譯器消化底層的領(lǐng)域和知識(shí)的差異性,取得了不錯(cuò)的效果。

    當(dāng)然,我們目前也有還不夠完善的地方,如:編譯器腳手架缺乏對(duì)于部分特性顆粒度更細(xì)的控制、代碼編譯轉(zhuǎn)換過(guò)程中日志的輸出更友好等等。后續(xù)我們也有計(jì)劃將 wepy 變色龍編譯器在社區(qū)開(kāi)源共建,屆時(shí)歡迎大家一起參與進(jìn)來(lái)。

    現(xiàn)階段編譯在前端的使用場(chǎng)景越來(lái)越多,或許我們真的進(jìn)入了Compiler is our framework的時(shí)代。

    【OSCHINA 2022 中國(guó)開(kāi)源開(kāi)發(fā)者問(wèn)卷】來(lái)啦

    你的反饋將有助于反映中國(guó)開(kāi)源的全貌

    問(wèn)卷結(jié)尾還可抽取我們的周邊好物哦~

    期待來(lái)自你的反饋!

    END

    歷史總是驚人的相似,但又從來(lái)不是簡(jiǎn)單的重復(fù)

    這里有最新開(kāi)源資訊、軟件更新、技術(shù)干貨等內(nèi)容

    點(diǎn)這里 ↓↓↓ 記得 關(guān)注? 標(biāo)星? 哦~返回搜狐,查看更多

    責(zé)任編輯:

    掃描二維碼推送至手機(jī)訪問(wèn)。

    版權(quán)聲明:本文由財(cái)神資訊-領(lǐng)先的體育資訊互動(dòng)媒體轉(zhuǎn)載發(fā)布,如需刪除請(qǐng)聯(lián)系。

    本文鏈接:http://www.daniuzhishi.com/?id=44551

    “從wepy到uniapp變形記” 的相關(guān)文章

    孩子喜歡足球,怎樣講解足球規(guī)則,才能讓孩子更加了解足球?

    孩子喜歡足球,怎樣講解足球規(guī)則,才能讓孩子更加了解足球?

    如果只把落點(diǎn)放在看懂規(guī)則、看懂比賽上,那真是無(wú)視這種運(yùn)動(dòng)的強(qiáng)大魅力。 足球是世界第一運(yùn)動(dòng)。世界杯是比奧運(yùn)會(huì)更受人矚目的世界最高級(jí)別體育賽事。世界杯能教給孩子的遠(yuǎn)比了解規(guī)則、看懂比賽多得多。 1??比分落后的球員們,到最后一刻都是殊死奮戰(zhàn)(加納),可以教會(huì)孩子永不言...

    競(jìng)彩足球|2022年世界杯揭幕戰(zhàn)!卡塔爾 VS 厄瓜多爾

    競(jìng)彩足球|2022年世界杯揭幕戰(zhàn)!卡塔爾 VS 厄瓜多爾

    周日 11-20 23:59 世界杯 卡塔爾 VS 厄瓜多爾 賽事分析: 卡塔爾歷史上從沒(méi)有參加過(guò)世界杯,若非東道主的原因,卡塔爾參加世界杯恐怕還得繼續(xù)延續(xù)多年??ㄋ柋煌饨绶Q為最弱的東道主。當(dāng)然卡塔爾也沒(méi)有這么不堪,畢竟他們是最近一屆的亞洲杯冠軍...

    球探比分:勝利總是對(duì)球隊(duì)有所幫助 我不在乎場(chǎng)上踢哪個(gè)位置

    球探比分:勝利總是對(duì)球隊(duì)有所幫助 我不在乎場(chǎng)上踢哪個(gè)位置

    點(diǎn)擊上方藍(lán)字,獲取更多大神球料。 在本輪西甲聯(lián)賽的一場(chǎng)比賽中,巴薩主場(chǎng)3-0擊敗比利亞雷亞爾,賽后,巴薩的荷蘭中場(chǎng)德容接受了媒體的采訪。 ——個(gè)人狀況 我之前有一些疲憊,小腿后部肌肉有些超負(fù)荷,教練做出的改變很好。 ——本場(chǎng)比賽 在經(jīng)歷與國(guó)米和皇馬的...

    競(jìng)彩足球世界杯冠軍,足球競(jìng)彩網(wǎng)世界杯?

    競(jìng)彩足球世界杯冠軍,足球競(jìng)彩網(wǎng)世界杯?

            感謝球迷朋友們一路的支持與陪伴,根據(jù)公眾號(hào)留言信息反饋比較多的是能不能建個(gè)交流群,應(yīng)大家的要求小編建立了一個(gè)球迷交流群,大家可以在這里分享一些賽事預(yù)告以及分析,轉(zhuǎn)會(huì)信息,游戲平臺(tái)介紹以及看球平臺(tái)    &...

    國(guó)家德比決定誰(shuí)才是2017年之王!即時(shí)比分皇馬44:43巴薩

    國(guó)家德比決定誰(shuí)才是2017年之王!即時(shí)比分皇馬44:43巴薩

    本周六,西甲首回合國(guó)家德比將在伯納烏球場(chǎng)開(kāi)踢,無(wú)論是對(duì)于皇馬還是巴薩,如果能以一場(chǎng)國(guó)家德比的勝利結(jié)束2017年,那真是再好不過(guò)了。 [MEMO url="http://v.img.pplive.cn/cp120/9f/45/9f453b4aa2d36b8a0714bc472e308...

    標(biāo)準(zhǔn)足球場(chǎng)尺寸有多大?

    標(biāo)準(zhǔn)足球場(chǎng)尺寸有多大?

    關(guān)注我們更多精彩等你發(fā)現(xiàn)!標(biāo)準(zhǔn)足球場(chǎng)尺寸有多大? 足球運(yùn)動(dòng)是一項(xiàng)古老的體育活動(dòng),源遠(yuǎn)流長(zhǎng)。足球是很讓人激動(dòng)不已的運(yùn)動(dòng),你知道標(biāo)準(zhǔn)足球場(chǎng)尺寸有多大嗎? 1、國(guó)際足球場(chǎng)面積標(biāo)準(zhǔn)規(guī)格 場(chǎng)地:長(zhǎng)105米、寬68米; 球門(mén):長(zhǎng)7.3...

    ?