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

微信Android 模塊化架構(gòu)重構(gòu)實(shí)踐(上)

開發(fā) 開發(fā)工具
微信Android誕生之初,用的是常見的分層結(jié)構(gòu)設(shè)計(jì)。這種架構(gòu)簡(jiǎn)單、清晰并一直沿襲至今。這是微信架構(gòu)的v1.x時(shí)代。

微信Android架構(gòu)歷史

微信Android誕生之初,用的是常見的分層結(jié)構(gòu)設(shè)計(jì)。這種架構(gòu)簡(jiǎn)單、清晰并一直沿襲至今。這是微信架構(gòu)的v1.x時(shí)代。

圖1-架構(gòu)演進(jìn)

圖1-架構(gòu)演進(jìn)

到了微信架構(gòu)的v2.x時(shí)代,隨著業(yè)務(wù)的快速發(fā)展,消息通知不及時(shí)和Android 2.3版本之前webview內(nèi)存泄露問(wèn)題開始突顯。由于代碼、內(nèi)存、apk大小都在增長(zhǎng),對(duì)系統(tǒng)資源的占用越來(lái)越多,導(dǎo)致微信進(jìn)程容易被系統(tǒng)回收。因此微信開始轉(zhuǎn)向多進(jìn)程架構(gòu),獨(dú)立的通信進(jìn)程保持長(zhǎng)連接的穩(wěn)定性,獨(dú)立的webview進(jìn)程也阻隔了內(nèi)存泄露導(dǎo)致的問(wèn)題。

時(shí)間繼續(xù)推進(jìn),我們也遇到了65535問(wèn)題和LinearAlloc問(wèn)題。這時(shí)的微信已經(jīng)具備了許多功能像朋友圈、搖一搖、附近的人等等,分離核心功能和其他業(yè)務(wù)模塊變得越發(fā)重要。為此,微信開啟了第三次架構(gòu)改造(v3.x)。我們對(duì)各種產(chǎn)品功能進(jìn)行解耦并拆分到相互獨(dú)立的p_xxx工程中,這是微信***次進(jìn)行模塊化架構(gòu)的重構(gòu)。經(jīng)過(guò)幾個(gè)月的努力,微信拆出了幾十個(gè)p工程,它們都通過(guò)基礎(chǔ)組件訪問(wèn)網(wǎng)絡(luò)、存儲(chǔ)等服務(wù),互相獨(dú)立并行。新的p工程架構(gòu)支撐了微信更快速的業(yè)務(wù)發(fā)展,配合多分支開發(fā)模式的改進(jìn),能夠支持團(tuán)隊(duì)多分支多team的并行開發(fā)。

圖2 - 架構(gòu)圖

圖2 - 架構(gòu)圖

為何再次重構(gòu)微信

原本好好的架構(gòu)出了什么問(wèn)題?

從上個(gè)架構(gòu)之后的兩年多時(shí)間里,微信Android基本沒有大的架構(gòu)改動(dòng)。配合gradle的編譯,以及git的多分支并行開發(fā),微信的模塊工程數(shù)量不斷增多,支撐了游戲、支付等大功能,可以說(shuō)這段時(shí)間里原有架構(gòu)起到了很好的作用。

然而隨著代碼繼續(xù)膨脹,一些問(wèn)題開始突顯出來(lái)。首先出問(wèn)題的是基礎(chǔ)工程libnetscene和libplugin?;A(chǔ)工程一直處于不斷膨脹的狀態(tài),同時(shí)主工程也在不斷變大。同時(shí)基礎(chǔ)工程存在中心化問(wèn)題,許多業(yè)務(wù)Storage類被附著在一個(gè)核心類上面,久而久之這個(gè)類已經(jīng)沒法看了。此外當(dāng)初為了平滑切換到gradle避免結(jié)構(gòu)變化太大以及太多module,我們將所有工程都對(duì)接到一個(gè)module上。缺少了編譯上的隔離,模塊間的代碼邊界出現(xiàn)一些劣化。雖然緊接著開發(fā)了工具來(lái)限制模塊間的錯(cuò)誤依賴,但這段時(shí)間里的影響已經(jīng)產(chǎn)生。在上面各種問(wèn)題之下,許多模塊已經(jīng)稱不上“獨(dú)立”了。所以當(dāng)我們重新審視代碼架構(gòu)時(shí),以前良好模塊化的架構(gòu)設(shè)計(jì)已經(jīng)逐漸變了樣。

 

圖3 - 架構(gòu)逐漸的變化

圖3 - 架構(gòu)逐漸的變化

“君有疾在腠理,不治將恐深”,在我們還在猶豫到底要不要重構(gòu)的時(shí)候,硬件同學(xué)向我們提出了需求。希望將微信Android代碼移植到類似微信相冊(cè)這樣產(chǎn)品中。這樣就可以快速跟進(jìn)微信業(yè)務(wù)***的支撐組件、協(xié)議、安全性、后臺(tái)服務(wù)等能力,而且代碼要盡可能精簡(jiǎn),可以選擇和定制模塊,可以移植模塊來(lái)實(shí)現(xiàn)原型嘗試。但就之前的情況來(lái)說(shuō),微信一時(shí)難以滿足。這下定了,還得重構(gòu)。

于是我們回過(guò)頭仔細(xì)看之前的設(shè)計(jì),找找問(wèn)題究竟是怎么來(lái)的。

問(wèn)題出在哪

先尋找代碼膨脹的原因。

翻開基礎(chǔ)工程的代碼,我們看到除了符合設(shè)計(jì)初衷的存儲(chǔ)、網(wǎng)絡(luò)等支持組件外,還有相當(dāng)多的業(yè)務(wù)相關(guān)代碼。這些代碼是膨脹的來(lái)源。但代碼怎么來(lái)的,非要放這?一切不合理皆有背后的邏輯。在之前的架構(gòu)中,我們大量適用Event事件總線作為模塊間通信的方式,也基本是唯一的方式。使用Event作為通信的媒介,自然要有定義它的地方,好讓模塊之間都能知道Event結(jié)構(gòu)是怎樣的。這時(shí)候基礎(chǔ)工程好像就成了存放Event的唯一選擇——Event定義被放在基礎(chǔ)工程中;接著,遇到某個(gè)模塊A想使用模塊B的數(shù)據(jù)結(jié)構(gòu)類,怎么辦?把類下沉到基礎(chǔ)工程;遇到模塊A想用模塊B的某個(gè)接口返回個(gè)數(shù)據(jù),Event好像不太適合?那就把代碼下沉到基礎(chǔ)工程吧……

就這樣越來(lái)越多的代碼很“自然的”被下沉到基礎(chǔ)工程中。

我們?cè)倏纯粗鞴こ?,它膨脹的原因不一樣。分析一下基本能確定的是,首先作為主干業(yè)務(wù)一直還有需求在開發(fā),膨脹在所難免,缺少適當(dāng)?shù)膬?nèi)部重構(gòu)但暫時(shí)不是問(wèn)題的核心。另一部分原因,則是因?yàn)槟K的生命周期設(shè)計(jì)好像已經(jīng)不滿足使用需要。之前的模塊生命周期是從“Account初始化”到“Account已注銷”,所以可以看出在這時(shí)機(jī)之外肯定還有邏輯。放在以前這不是個(gè)大問(wèn)題,剛啟動(dòng)還不等“Account初始化”就要執(zhí)行的邏輯哪有那么多。而現(xiàn)在不一樣,再簡(jiǎn)單的邏輯堆積起來(lái)也會(huì)變復(fù)雜。此時(shí),在模塊生命周期外的邏輯基本上只能放主工程。

此外的問(wèn)題,模塊邊界破壞、基礎(chǔ)工程中心化,都是代碼持續(xù)劣化的幫兇。

總之在模塊化上我們忽視了一些重要的問(wèn)題,必須重塑。

重塑模塊化

重塑模塊化,我們分解為三個(gè)目標(biāo):

  • 改變通信方式
  • 重新設(shè)計(jì)模塊
  • 約束代碼邊界

改變通信方式

前面講過(guò),我們使用Event總線作為模塊間通信的媒介,這種設(shè)計(jì)很常見。然而當(dāng)回顧整體代碼時(shí)能發(fā)現(xiàn),Event并非所有通信需要的***形式。它的特點(diǎn)適合一對(duì)多的廣播場(chǎng)景,依賴關(guān)系弱。一旦遇到需要一組業(yè)務(wù)接口時(shí),用Event寫起來(lái)那是十分痛苦的。也正因如此,這種情況下大家都跳過(guò)了Event的使用,直接將代碼下沉到了基礎(chǔ)工程,共享代碼,進(jìn)而導(dǎo)致基礎(chǔ)工程的不斷膨脹。

所以選個(gè)合適的通信方式很有必要,我們希望兼顧考慮開發(fā)的便利性和協(xié)議的約束性。

Event不合適。協(xié)議通信如何?

我們理解的協(xié)議通信,是指跨平臺(tái)/序列化的通信方式,類似終端和服務(wù)器間的通信或restful這種?,F(xiàn)在這種形式在終端內(nèi)很常見了。協(xié)議通信具備一種很強(qiáng)力解耦能力,但也有不可忽視的代價(jià)。無(wú)論什么形式的通信,所有的協(xié)議定義需要讓通訊兩方都能獲知。通常為了方便會(huì)在某個(gè)公共區(qū)域存放所有協(xié)議的定義,這情況和Event引發(fā)的問(wèn)題有點(diǎn)像。另外,協(xié)議如果變化了,兩端怎么同步就變得有點(diǎn)復(fù)雜,至少要配合一些框架來(lái)實(shí)現(xiàn)。在一個(gè)應(yīng)用內(nèi),這樣會(huì)不會(huì)有點(diǎn)復(fù)雜?用起來(lái)好像也不那么方便?更何況它究竟解決多少問(wèn)題呢。

所以我們想要簡(jiǎn)單點(diǎn)。經(jīng)過(guò)權(quán)衡,我們決定用模塊提供“SDK”的方式作為它與其他模塊進(jìn)行通信的手段。

通常“SDK”提供的是什么,是接口 + 數(shù)據(jù)結(jié)構(gòu)。這種方式好處明顯:實(shí)現(xiàn)簡(jiǎn)單也能解決問(wèn)題,IDE容易補(bǔ)全、調(diào)用接口方便,不用配合工具,協(xié)議變化直接反映在編譯上,維護(hù)接口也簡(jiǎn)單了。

其實(shí)想想,用協(xié)議的方式在終端內(nèi)作為通信手段,開發(fā)效率低,也容易出錯(cuò)。因此可能會(huì)誕生各種框架和工具來(lái)提升這里損失的效率。到頭來(lái),是不是大家都實(shí)現(xiàn)了一套類似RPC這樣的封裝。其實(shí)本地的通信,能用接口就挺好,不能用的時(shí)候,再用協(xié)議封裝也來(lái)得及。

確定了方案,實(shí)現(xiàn)起來(lái)就很簡(jiǎn)單。我們的注冊(cè)方式和接口訪問(wèn)都很簡(jiǎn)單。用接口注冊(cè),再用接口訪問(wèn),不暴露實(shí)現(xiàn)細(xì)節(jié)。如下圖。

圖4 - 注冊(cè)接口

圖4 - 注冊(cè)接口

圖5 - 訪問(wèn)接口

圖5 - 訪問(wèn)接口

接下來(lái),怎么暴露接口更方便?

模塊暴露“SDK”的方式無(wú)非就是新建個(gè)“SDK”工程,剝離接口和數(shù)據(jù)結(jié)構(gòu)到該工程里面,然后讓其他模塊引用編譯。但這樣有點(diǎn)麻煩,能不能再方便點(diǎn)?

當(dāng)然有辦法。我們實(shí)現(xiàn)了另一種接口暴露的形式——“.api化”。

使用方式和思路都很簡(jiǎn)單。對(duì)于java文件,將工程里想要暴露出去的接口類后綴名從“.java”改成“.api”,就可以了。

而且并不只是java文件,其他文件如果也想暴露,在文件名后增加".api”,也一樣可以。

圖6 - “.api化”

圖6 - “.api化”

當(dāng)然,要讓工程支持這樣的方式,gradle文件肯定會(huì)有一點(diǎn)改變。

settings.gradle

build.gradle

圖 7 - settings.gradle & build.gradle

圖 7 - settings.gradle & build.gradle

就這樣,可以說(shuō)暴露接口變得非常容易,不用擔(dān)心實(shí)現(xiàn)類也被人引用到。而它的實(shí)現(xiàn)原理也相當(dāng)簡(jiǎn)單:自動(dòng)生成一個(gè)“SDK”工程,拷貝.api后綴文件到工程中就行了,后面其他工程依賴編譯的只是這個(gè)生成的工程。簡(jiǎn)單好用。

還有個(gè)細(xì)節(jié),如果想編輯.api后綴的java文件,為了能讓Android Studio繼續(xù)高亮該怎么辦?可以在File Type中把.api作為java文件類型。

圖8 - 設(shè)置File Types

圖8 - 設(shè)置File Types

重新設(shè)計(jì)模塊

要把模塊重新設(shè)計(jì),還要做好幾件事。首先,消滅代碼經(jīng)常下沉的“三不管區(qū)域”——基礎(chǔ)工程。這意味著原來(lái)的模塊要把之前下沉的代碼重新認(rèn)領(lǐng)回去。

圖9 - 分層結(jié)構(gòu)改造

圖9 - 分層結(jié)構(gòu)改造

為了鞏固替代基礎(chǔ)工程的mmkernel層,不被濫用為新的代碼堆放處,順便還要解決中心化問(wèn)題。就必須強(qiáng)化它的職責(zé)和設(shè)計(jì)。

mmkernel結(jié)構(gòu)可以很通用的定義為CoreAccount/CoreNetwork/CoreStorage三個(gè)部分,分別提供了核心賬號(hào)狀態(tài)(初始化、注銷)、網(wǎng)絡(luò)狀態(tài)回調(diào)(鏈接建立)、存儲(chǔ)狀態(tài)生命周期(db創(chuàng)建、銷毀、用戶存儲(chǔ)路徑切換、sdcard掛起)。

圖10

圖10

再然后是生命周期問(wèn)題,我們需要重新設(shè)計(jì)正確的生命周期。

之前講過(guò),我們的模塊生命周期大體上只有“Account初始化”和“Account注銷”兩個(gè)階段。這已經(jīng)不夠用了。

所以擴(kuò)大模塊的生命周期,就給了模塊實(shí)現(xiàn)各種代碼需要的時(shí)機(jī),才能避免大家往主工程塞代碼。

圖11

圖11

實(shí)現(xiàn)新的生命周期是一個(gè)正確的選擇,同時(shí)產(chǎn)生了解決另一個(gè)問(wèn)題的機(jī)會(huì)——復(fù)雜的啟動(dòng)流程。

要知道主工程的代碼一部分原因是啟動(dòng)流程堆積造成的,邏輯多了代碼自然多。隨之而來(lái)的問(wèn)題就是代碼多了,邏輯也就跟著復(fù)雜起來(lái)。微信的初始化邏輯是順序排列在一起并從上到下執(zhí)行,某種情況下還會(huì)異步啟動(dòng)。當(dāng)程序啟動(dòng)流程比較復(fù)雜時(shí),這樣的代碼會(huì)產(chǎn)生“隱性依賴”的問(wèn)題。“隱性依賴”顧名思義就是:原本并應(yīng)該存在依賴的代碼,隨著版本的迭代逐漸產(chǎn)生了依賴,而且還不明顯。這樣的情況會(huì)讓情況惡化,大家只敢往里面堆代碼,但卻不敢“亂動(dòng)”。

所以重新設(shè)計(jì)的模塊應(yīng)該要徹底避免這些問(wèn)題。

我們重新定義了模塊的生命周期,將模塊的生命周期延長(zhǎng)到應(yīng)用啟動(dòng)和退出。而后,每個(gè)模塊都可以定義一個(gè)Plugin類,作為模塊的“支柱”或“起點(diǎn)”。作為解決初始化問(wèn)題的手段,它具備幾個(gè)主要階段:dependency()、configure()、execute()

圖12 - plugin初始化的幾個(gè)階段

圖12 - plugin初始化的幾個(gè)階段

dependency()階段,用于設(shè)置需要依賴的其他Plugin,當(dāng)然提供那個(gè)Plugin的別名接口類就可以了。

圖13 - 設(shè)置dependency

圖13 - 設(shè)置dependency

依賴階段我們會(huì)生成整個(gè)模塊的依賴樹。這與編譯時(shí)的依賴不同。通常的依賴關(guān)系是分為兩種的,一種是類型依賴也就是編譯期依賴,需要被依賴模塊提供具體類型才能編譯通過(guò);另一種依賴則是運(yùn)行期的邏輯依賴或數(shù)據(jù)一致性依賴,當(dāng)一個(gè)模塊用這種方式依賴另一個(gè)模塊,就意味著,前者的執(zhí)行要依賴后者執(zhí)行已完成,通常是為了數(shù)據(jù)準(zhǔn)備妥當(dāng)或保證所需服務(wù)已被注冊(cè)。

顯性的運(yùn)行期依賴把之前啟動(dòng)邏輯的“隱性依賴”完全暴露在陽(yáng)光之下,改啟動(dòng)邏輯不用提心吊膽。

圖14 - 依賴關(guān)系樹狀圖

圖14 - 依賴關(guān)系樹狀圖

configure()階段,該階段是根據(jù)之前的依賴樹遍歷執(zhí)行。通常用于初始化一些數(shù)據(jù)配置、注冊(cè)IService服務(wù)、向前面依賴的模塊注冊(cè)一些回調(diào)等等。

此外這個(gè)階段還有額外的作用是插入BootTask,用于后面execute()階段的執(zhí)行。

圖15 - configure階段

圖15 - configure階段

execute()階段,為了改變啟動(dòng)流程不清楚的情況,強(qiáng)調(diào)啟動(dòng)邏輯之間的依賴關(guān)系,我們現(xiàn)在將每個(gè)要執(zhí)行的啟動(dòng)步驟封裝為BootTask。前面的configure階段時(shí),我們可以將BootTask插入到通過(guò)dependency()得到的依賴樹。每個(gè)Plugin同時(shí)也是一個(gè)BootTask,也因此擁有execute()接口。最終得到了包含所有BootTask的啟動(dòng)樹,將遍歷執(zhí)行所有節(jié)點(diǎn)執(zhí)行execute()。

 

圖16 - BootTask

圖16 - BootTask

獨(dú)立使用BootTask的方式并不十分常見,通常Plugin本身的execute已經(jīng)夠用。不過(guò)在一些通用型組件初始化嘗試會(huì)需要用到,如某些給某個(gè)全局使用的預(yù)加載資源提前初始化的邏輯。

為何設(shè)計(jì)configure()和execute(),這可以理解為“收集任務(wù)”和“執(zhí)行任務(wù)”的兩個(gè)階段。另外這樣的抽象還可以實(shí)現(xiàn)從外部調(diào)度execute的執(zhí)行線程,將啟動(dòng)邏輯和啟動(dòng)異步代碼分開。順便解決了,原來(lái)異步啟動(dòng)代碼混亂不堪的情況。

約束代碼邊界

從之前的經(jīng)驗(yàn)看,要想約束好代碼的邊界不被破壞,編譯上的隔離是唯一法寶。

除了工程和工程之間的分割,在工程的內(nèi)部如果也能實(shí)現(xiàn)約束代碼就更好了,算是將問(wèn)題扼殺在搖籃里。之前通常的做法是以module工程為單位的相互分離,但在工程內(nèi)部并不限制代碼相互引用。所以為了規(guī)范代碼,常能看到用包名作為約定,區(qū)分內(nèi)部功能職責(zé),靠約定維持解耦。隨著時(shí)間推移,很快就能發(fā)現(xiàn)包名約定作為約束太弱了,在快速迭代的代碼上很難一直維持下去。不管怎么樣解決,總要通過(guò)一些手段審查代碼引用的對(duì)不對(duì)。感覺有點(diǎn)防不勝防。為此,我們實(shí)現(xiàn)了一種簡(jiǎn)單易用、粒度更細(xì)的工程組織結(jié)構(gòu)——pins工程結(jié)構(gòu)

圖18 - code-check

圖18 - code-check

這樣的工程組織形式的兩個(gè)明顯好處:

  • 約束代碼粒度和小代碼邊界的利器

粒度極小,一個(gè)pin工程也許只有一個(gè)源文件,只要它能表達(dá)一個(gè)獨(dú)立職責(zé)。對(duì)于任何一個(gè)模塊,從內(nèi)部約束自己的功能結(jié)構(gòu),是對(duì)整體代碼邊界約束的極大補(bǔ)充。以前面插的結(jié)構(gòu)為例,一個(gè)gallery業(yè)務(wù)可能提供了幾種不同的產(chǎn)品功能,以及支撐能力。那么將其相互獨(dú)立的代碼進(jìn)行區(qū)分,避免混雜,就會(huì)顯得十分必要。清晰的結(jié)構(gòu),意味著后期維護(hù)成本的降低和開發(fā)效率的提高,留下了靈活性。

  • 避免的超量module的創(chuàng)建,輕量

pins工程某種程度上能減少一些粒度太小的module工程,也一定程度的緩解太多module工程時(shí)的gradle編譯性能問(wèn)題。

至此,我們基本完成了重塑模塊化的設(shè)計(jì)目標(biāo),解決掉很多之前沒有考慮的問(wèn)題。算是模塊化的加強(qiáng)版。另外設(shè)計(jì)是一方面,拆分解耦原來(lái)代碼以及遷移還是另一回事,這個(gè)過(guò)程也是十分艱難和枯燥這里就不細(xì)講了。接下來(lái)想辦法看看重構(gòu)的效果。

看看效果

重新設(shè)計(jì)的模塊化加上代碼的重構(gòu)。我們終于能滿足之前硬件同學(xué)的需要。同時(shí)一并解決許多拖欠的問(wèn)題。

在編譯上,整體編譯速度會(huì)因?yàn)閙odule增多而下降一些。但拆分module之后,卻能顯著加快單工程增量編譯的速度。和之前相比,一行代碼的增量編譯耗時(shí)能減少60%。

除了滿足需求外,架構(gòu)設(shè)計(jì)的效果并不好量化,不過(guò)我們嘗試用一個(gè)demo來(lái)說(shuō)明。

WeChat nano

基于前面介紹過(guò)的輕量的微信內(nèi)核mmkernel層,再配合一個(gè)不包含界面的基礎(chǔ)聊天模塊和Auth模塊,可以在短時(shí)間里開發(fā)出一個(gè)及精簡(jiǎn)版本的微信——WeChat nano。

圖19 - WeChat nano

圖19 - WeChat nano

模擬這個(gè)console的界面是單獨(dú)開發(fā)的,時(shí)間的大頭都花在這上面。

它的效果不錯(cuò):

  • 可以讓安裝包大小縮減到3.5M,大概是完整版本的10%
  • 能大幅減小內(nèi)存占用,約占用完整版本的25% (注:只計(jì)算應(yīng)用相關(guān)有不同的部分PSS)

大概就是這樣。

下篇:http://zhuanlan.51cto.com/art/201708/547813.htm

原文鏈接:https://www.qcloud.com/community/article/441423  作者:carlguo

【本文是51CTO專欄作者“騰訊云技術(shù)社區(qū)”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)51CTO聯(lián)系原作者獲取授權(quán)】

戳這里,看該作者更多好文

 

責(zé)任編輯:武曉燕 來(lái)源: 51CTO專欄
相關(guān)推薦

2017-08-11 16:10:36

微信Android實(shí)踐

2017-05-18 11:43:41

Android模塊化軟件

2015-07-02 13:21:44

模塊化數(shù)據(jù)中心

2013-08-20 16:45:22

重構(gòu)Web App模塊化

2016-12-14 14:50:26

CSS預(yù)處理語(yǔ)言模塊化實(shí)踐

2010-02-03 09:01:01

Java動(dòng)態(tài)模塊化

2019-08-28 16:18:39

JavaScriptJS前端

2021-10-11 09:51:37

模塊化UPS架構(gòu)

2017-02-13 18:46:38

Android模塊化組件化

2017-06-09 10:06:54

微信小程序架構(gòu)分析

2016-11-08 20:31:19

同方服務(wù)器模塊化

2022-01-10 08:43:25

CanonicalSnap應(yīng)用Linux

2023-06-28 08:12:49

Python代碼重構(gòu)

2021-12-24 07:10:36

架構(gòu)分層模塊化

2017-07-11 11:02:03

APP模塊化架構(gòu)

2013-08-20 15:31:18

前端模塊化

2017-05-18 10:23:55

模塊化開發(fā)RequireJsJavascript

2020-09-17 10:30:21

前端模塊化組件

2015-10-10 11:29:45

Java模塊化系統(tǒng)初探

2020-09-18 09:02:32

前端模塊化
點(diǎn)贊
收藏

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