1. 背景
1.1 項(xiàng)目背景
公司近兩年快速發(fā)展,社區(qū)線(xiàn)C端代碼分散在不同倉(cāng)庫(kù)中,每個(gè)倉(cāng)庫(kù)中采用不同的前端框架和選型,且均含有幾條業(yè)務(wù)線(xiàn)的代碼,團(tuán)隊(duì)整體采用敏捷模式快速迭代,導(dǎo)致開(kāi)發(fā)管理成本較高,升級(jí)改造麻煩。比如,所關(guān)聯(lián)的三個(gè)倉(cāng)庫(kù)中的代碼均引了一個(gè)內(nèi)部基礎(chǔ)組件庫(kù),該組件有非必現(xiàn)bug,導(dǎo)致三個(gè)倉(cāng)庫(kù)的不同頁(yè)面均出現(xiàn)了不同表現(xiàn)的異常,由具體負(fù)責(zé)的不同測(cè)試分別報(bào)到前端開(kāi)發(fā),分別溝通、排查、解決并走獨(dú)立的發(fā)布上線(xiàn)流程,耗時(shí)耗力。當(dāng)同一倉(cāng)庫(kù)中活躍著不同業(yè)務(wù)線(xiàn)的開(kāi)發(fā),一個(gè)公共的地方需要修改,開(kāi)發(fā)沒(méi)有溝通清楚導(dǎo)致沖突線(xiàn)上bug。
此外,公司C端體驗(yàn)分析的統(tǒng)計(jì)和報(bào)表是應(yīng)用粒度的,先前代碼耦合了其他業(yè)務(wù)的內(nèi)容,導(dǎo)致我所在業(yè)務(wù)線(xiàn)的統(tǒng)計(jì)數(shù)據(jù)不置信。
近期團(tuán)隊(duì)對(duì)C端項(xiàng)目進(jìn)行重構(gòu),將不同倉(cāng)庫(kù)中的代碼匯總到一個(gè)倉(cāng)庫(kù)中管理。以期減少管理成本及方便后續(xù)對(duì)組內(nèi)項(xiàng)目做優(yōu)化和升級(jí)改造。
1.2 重構(gòu)經(jīng)驗(yàn)
之前我有獨(dú)立負(fù)責(zé)過(guò)幾次較大的重構(gòu),也曾2周獨(dú)立完成近20萬(wàn)行C端代碼(不含node_modules)從JS到TS遷移在并行業(yè)務(wù)需求迭代的情況下實(shí)現(xiàn)上線(xiàn)0bug。
1.3 重構(gòu)基礎(chǔ)
Q:什么是重構(gòu)?
重構(gòu)是在不改變軟件可觀(guān)察行為的前提下,改善其內(nèi)部結(jié)構(gòu)。–《重構(gòu) – 改善既有代碼的設(shè)計(jì)》
Q:為什么要重構(gòu)?
重構(gòu)可以提高理解性和降低修改成本 。–《重構(gòu) – 改善既有代碼的設(shè)計(jì)》
Q:什么時(shí)候重構(gòu)?
(1)何時(shí)不應(yīng)該重構(gòu)?
沒(méi)有價(jià)值,沒(méi)有意義或者投入產(chǎn)出比很低時(shí)。團(tuán)隊(duì)資源是有限的,有限的資源應(yīng)該盡可能投入到有意義的事情上去。從團(tuán)隊(duì)的角度考慮投入產(chǎn)出比,對(duì)于已經(jīng)只是維護(hù)狀態(tài),如無(wú)需求、無(wú)調(diào)整的代碼,不要去動(dòng)它,如果對(duì)于新手而言,不僅不會(huì)帶來(lái)好處反而可能挖坑,要知道既有代碼可能有不少坑。
(2)何時(shí)應(yīng)該重構(gòu)?
- 項(xiàng)目維護(hù)成本很高
- 影響項(xiàng)目調(diào)優(yōu),如性能優(yōu)化時(shí)
- 代碼長(zhǎng)得丑,不優(yōu)雅時(shí)
- 既有設(shè)計(jì)和實(shí)現(xiàn)不利于擴(kuò)展新功能時(shí)
- 重復(fù)性工作,既有的代碼無(wú)法幫助你輕松添加新特性時(shí)
- 修補(bǔ)bug時(shí),排查邏輯困難
- Code Review 可以讓他人來(lái)復(fù)審代碼檢查是否具備可讀性,可理解性
- 太多的代碼無(wú)注釋?zhuān)讶贿B自己都無(wú)法快速理清代碼邏輯
1.4 如何重構(gòu)
(1)準(zhǔn)備(基本功)
推薦值得一讀再讀經(jīng)典書(shū)籍,重構(gòu)圣經(jīng)《重構(gòu) – 改善既有代碼的設(shè)計(jì)》。本人從畢業(yè)第一年開(kāi)始,幾年下來(lái)讀了4遍 ,受益匪淺,每次復(fù)習(xí)都能有所收獲,讓我經(jīng)常折騰經(jīng)手的項(xiàng)目卻沒(méi)出過(guò)問(wèn)題。
(2)重構(gòu)實(shí)踐要點(diǎn)
- 思考清楚(整體有設(shè)計(jì),不一定要文檔化但需要想清楚)。
- 協(xié)同規(guī)劃(開(kāi)發(fā)團(tuán)隊(duì)內(nèi)部的配合及重構(gòu)分支與其他分支的集成、外部資源提前申請(qǐng)如產(chǎn)品、測(cè)試、運(yùn)維等)、整體規(guī)劃。
- 分層分步展開(kāi),抓大放小從粗到細(xì)。善用“批處理”。
- 一次只做一件事。
- 不要重復(fù)造輪子。
- 當(dāng)你覺(jué)得一件事很難的時(shí)候,停下來(lái)思考是不是方法用錯(cuò)了,它應(yīng)該是怎樣的。保持監(jiān)控及復(fù)盤(pán)自己的思考方式。
- 做好對(duì)內(nèi)和對(duì)外溝通,尤其在當(dāng)項(xiàng)目不是只有一個(gè)人在開(kāi)發(fā)和維護(hù)的情況下。注意提前和相關(guān)方(測(cè)試、運(yùn)維)溝通好(方案、主要時(shí)間節(jié)點(diǎn)、需要投入的資源、需要其配合的事項(xiàng))。
2. 社區(qū)C端的重構(gòu)實(shí)踐
本次重構(gòu)具有一定的復(fù)雜度,除了技術(shù)遷移改造的成本外,涉及的幾個(gè)倉(cāng)庫(kù)是不同技術(shù)選型(框架&上層組件等)、項(xiàng)目快速的敏捷迭代、需求高并發(fā)及多人協(xié)同開(kāi)發(fā)維護(hù)狀態(tài)。
2.1 現(xiàn)狀分析
技術(shù)棧:
倉(cāng)庫(kù)名 | 技術(shù)棧 | 社區(qū)C端頁(yè)面數(shù) |
repo A | React umi3 | 目標(biāo)倉(cāng)庫(kù)無(wú)需統(tǒng)計(jì) |
repo B | react umi3 | 5 |
repo C | vue2 vuex | 27 |
項(xiàng)目側(cè)
三個(gè)倉(cāng)庫(kù) A / B / C 更新活躍,每個(gè)倉(cāng)庫(kù)均涉及多業(yè)務(wù)線(xiàn)的開(kāi)發(fā),并行維護(hù)。分別按照2周一個(gè)sprint的迭代節(jié)奏展開(kāi),1周開(kāi)發(fā)1周測(cè)試,間或穿插著hotfix。
從 V1主版本發(fā)布后開(kāi)始重構(gòu),各個(gè)倉(cāng)庫(kù)涉及的代碼如下:
- repo A:A1 A1.* A2 A2.*
- repo B:B1 B1.* B2 B2.*
- repo C:C1 C1.* C2 C2.*
.*表示hotfix
2.2 重構(gòu)計(jì)劃
前端側(cè)的整體思路:
- repo A 較新,是社區(qū)的主要倉(cāng)庫(kù),集中了大部分C端頁(yè)面,作為目標(biāo)C端代碼的目標(biāo)倉(cāng)庫(kù)。
- repo B 到 repo A:repo B 與 目標(biāo)倉(cāng)庫(kù)的技術(shù)棧很接近,涉及5個(gè)頁(yè)面,通過(guò)人肉方式遷移,過(guò)程中注意依賴(lài)的一并遷移。
- repo C 到 repo A:repo C 與目標(biāo)倉(cāng)庫(kù)差異較大,且語(yǔ)言異構(gòu),上層框架、組件庫(kù)等都有較大差異,涉及頁(yè)面較多。
- 首先確定有效的頁(yè)面,將已下線(xiàn)頁(yè)面的dead code排除在遷移范圍之外;具體細(xì)節(jié)下文會(huì)說(shuō)到,取出待遷移倉(cāng)庫(kù)中的前端路由配置,知道頁(yè)面總范圍,查看阿里云sls日志中近期的PV(兩種查詢(xún)方式校對(duì)),排除無(wú)流量的頁(yè)面。
- 分層分級(jí)重構(gòu),前期抓大放小,耗時(shí)耗力還容易出問(wèn)題的框架語(yǔ)法轉(zhuǎn)換(vue to react)應(yīng)采用腳本工具化實(shí)現(xiàn),實(shí)現(xiàn)文件級(jí)和各個(gè)類(lèi)中整體結(jié)構(gòu)及引用關(guān)系的維護(hù)的轉(zhuǎn)換。
- 細(xì)節(jié)語(yǔ)法通過(guò)自定義腳本批處理(比如 vue中用的 class的key和字符串形式的value轉(zhuǎn)換成react中的className及變量形式的value)。
- 為保證遷移后高效自測(cè)需要將對(duì)應(yīng)的 *.vue 文件保留,將其看成doc文件,待整個(gè)遷移完畢再刪除,以提升遷移及測(cè)試的效率。注意改造lint規(guī)則忽視對(duì)這類(lèi)文件的檢測(cè)。
- 過(guò)程中依賴(lài)文件一同遷入,有“名稱(chēng)空間隔離”,注意保持整體目錄結(jié)構(gòu)的相對(duì)關(guān)系,做整體遷移,且不去污染目標(biāo)倉(cāng)庫(kù)中的既有文件,防止同名文件覆蓋的情況。
通過(guò)上述三步將各個(gè)倉(cāng)庫(kù)代碼遷移到 repo A 后,同步 三個(gè)倉(cāng)庫(kù)中的最新更新。repo C 到 repo A 的過(guò)程中(從V1 切出的分支),repo C 還在持續(xù)更新代碼,repo A 還需要將 repo C 中的 V1.*、V2、V2.* 代碼合入(repo B亦然)。由于代碼都在不同的倉(cāng)庫(kù)中,需要手工合并。Tips:可以在 repo C 中將 V1.*、V2、V2.* 的多個(gè)commits合成一個(gè)commit,將所有變更項(xiàng)匯總到一處做批量更新。
repo A 中 SSR方案調(diào)研和應(yīng)用也在并行。重構(gòu)中新遷入的頁(yè)面要和SSR做集成。
2.3 重構(gòu)與集成實(shí)踐
2.3.1 倉(cāng)庫(kù)B頁(yè)面梳理及遷入
這部分遷移在同構(gòu)語(yǔ)言中進(jìn)行,且涉及頁(yè)面數(shù)不多,主要通過(guò)人為遷移。
2.3.2 倉(cāng)庫(kù)C頁(yè)面梳理及遷入
- 線(xiàn)上流量查詢(xún),排除無(wú)用頁(yè)面
- 三個(gè)代碼倉(cāng)庫(kù)中路由申明確定總范圍
- 根據(jù)阿里云日志確定過(guò)去3個(gè)月、2個(gè)月、1個(gè)月中的PV,將無(wú)PV的頁(yè)面從待遷移頁(yè)面池中剔除。
- 注意1: 阿里云SLS日志是基于上報(bào)的數(shù)據(jù),上報(bào)和統(tǒng)計(jì)過(guò)程可能有丟數(shù)據(jù)的情況,所以綜合兩個(gè)查詢(xún)?nèi)肟诖_定和排查。
- 注意2: 對(duì)于有1-2個(gè)PV的頁(yè)面,可能是團(tuán)隊(duì)內(nèi)部開(kāi)發(fā)前期做調(diào)研時(shí)產(chǎn)生的,確定訪(fǎng)問(wèn)者后排出“測(cè)試”產(chǎn)生PV的頁(yè)面。
- 確定最終重構(gòu)范圍(27個(gè)過(guò)濾13個(gè))。將步驟1中獲取的總范圍中在步驟2中無(wú)用戶(hù)PV的頁(yè)面剔除。
- 異構(gòu)語(yǔ)言轉(zhuǎn)換和處理
- 工具轉(zhuǎn)換
- 倉(cāng)庫(kù)C中Vue2 轉(zhuǎn)換為倉(cāng)庫(kù)A中的react
這里主要用到了 vue-to-react,然而該工具有不少約束和限制,大概成功轉(zhuǎn)換了一半的代碼,轉(zhuǎn)化失敗的情況需要自己寫(xiě)腳本實(shí)現(xiàn)。原想對(duì)該庫(kù)的源碼進(jìn)行二次封裝和改造,看了其實(shí)現(xiàn)發(fā)現(xiàn)定制的成本高于自己寫(xiě)腳本的成本所以棄了(本人vue的經(jīng)驗(yàn)一個(gè)月不到),時(shí)間太緊不容仔細(xì)去研究。Tips:避免重復(fù)造輪子,當(dāng)執(zhí)行很繁瑣且很多重復(fù)的動(dòng)作時(shí),可以考慮擁抱團(tuán)隊(duì)內(nèi)部的輪子、社區(qū)和開(kāi)源,沒(méi)有的話(huà)就自己去倒騰一個(gè)。
- 腳本轉(zhuǎn)換
- 轉(zhuǎn)換
- 項(xiàng)目目錄結(jié)構(gòu)設(shè)計(jì)及文件的映射過(guò)程
// step1:保持整體目錄結(jié)構(gòu)的相對(duì)性不變.├── apis│ ├── community.ts│ ├── h5community│ ├── ...├── components├── pages│ ├── h5community│ │ ├── App│ │ ├── api│ │ ├── asset│ │ ├── components│ │ ├── config│ │ ├── filter│ │ ├── live.js│ │ ├── main.js│ │ ├── mixins.js│ │ ├── router│ │ ├── style│ │ ├── utils│ │ └── views│ ├── community├── utils└── ...// step2: foo.vue文件轉(zhuǎn)為 foo/ 目錄,模板分別映射為jsx及l(fā)ess文件.├── apis│ ├── community.ts│ ├── h5community│ └── ...├── components│ ├── h5community│ └── ...├── config│ ├── h5community.js│ └── ...├── pages│ ├── community│ └── h5community│ ├── column // 原 column.vue 轉(zhuǎn)為目錄,分拆成index.tsx及index.scss│ │ ├── index.local_js // index.local_js作為注釋保留,用于測(cè)試回歸的參考│ │ ├── index.scss│ │ └── index.tsx // 首行自動(dòng)插入對(duì) index.scss 的引用│ └── ...└── utils ├── h5community └── ...
- 分步轉(zhuǎn)換1: 文件級(jí)
對(duì)于 vue-to-react 處理失敗的頁(yè)面,通過(guò)腳本生成頁(yè)面模版文件。
// 轉(zhuǎn)換前文件為 foo.vue// 轉(zhuǎn)換后:.└── foo ├── index.jsx ├── index.local_js └── index.scss
自定義腳本轉(zhuǎn)換生成的文件內(nèi)容結(jié)構(gòu)如下:
- 分步轉(zhuǎn)換2: 語(yǔ)法級(jí)-html lang
Vue 文件轉(zhuǎn)換過(guò)程中有很多 lang="pug"類(lèi)的模版,通過(guò)工具 https://pughtml.com/ 轉(zhuǎn)換成“類(lèi)jsx”的模版(但凡雞肋人肉的事,首先應(yīng)該想到工具,如果找不到,不妨Google中嘗試用不同的關(guān)鍵詞,而不要去人工)。
// 轉(zhuǎn)換前 foor.vue 中<template lang="pug"> article.modal-wrap(@touchmove.stop.prevent @click.stop='close') section.modal p.more 更多精彩內(nèi)容, 就在得物App p.slogan 有毒的運(yùn)動(dòng) x 潮流 x 好物 .enter-btn(@click.stop='enter') 進(jìn)入得物App aside.close(@click.stop='close')</template>// 轉(zhuǎn)換后 foo/index.jsx 中<article class="modal-wrap" @touchmove.stop.prevent="@touchmove.stop.prevent" @click.stop="close"> <section class="modal"> <p class="more">更多精彩內(nèi)容, 就在得物App</p> <p class="slogan">有毒的運(yùn)動(dòng) x 潮流 x 好物</p> <div class="enter-btn" @click.stop="enter">進(jìn)入得物App</div> <aside class="close" @click.stop="close"></aside> </section></article>
- 分步轉(zhuǎn)換3: 語(yǔ)法級(jí)-className等
上面腳本生成的文件在于文件級(jí)的轉(zhuǎn)換,語(yǔ)法差異需要腳本解決。比如 class的替換和解析。這里 html 屬性的規(guī)則解析正則比較繁瑣,實(shí)現(xiàn)時(shí)會(huì)思考哪里會(huì)有,很自然就想到了vue的源碼中一定會(huì)有該正則(框架是要解析做原生映射的),查了下果不其然,稍作修改就可以了,然后再做些定制(業(yè)務(wù)代碼中的模版代碼,如import style這些用腳本自動(dòng)生成按需插入)。
// foo.vue 文件中的寫(xiě)法 <div class="var1">demo1</div><div class="var1 var2">demo1</div>// foo/index.jsx (react中)的寫(xiě)法import style from './index.scss'import classNames from 'classnames'...<div className={style["var1"]}>demo1</div><div className={classNames(style["var1"], style["var2"])}>demo1</div>
- 逐頁(yè)面調(diào)試與校對(duì)
- 倉(cāng)庫(kù)技術(shù)選型間的差異問(wèn)題
- umi的路由規(guī)則與定制
- 第三方組件庫(kù)
- 如Swiper、postcss-px-to-viewport等,vue版與react版有些差異,文檔不全,擁抱源碼和社區(qū)。其中postcss-px-to-viewport在不同倉(cāng)庫(kù)中使用不同的viewportWidth設(shè)置,轉(zhuǎn)換過(guò)程中通過(guò)對(duì)不同的插件實(shí)例處理不同的路徑范圍實(shí)現(xiàn)
- 基本功:敏感度(這個(gè)跟經(jīng)驗(yàn)有關(guān))。庫(kù)定位是什么?成熟度怎么樣?應(yīng)該有什么不應(yīng)該支持什么?如果自己來(lái)設(shè)計(jì)大概會(huì)怎么設(shè)計(jì)(有時(shí)候即使文檔不全情況下,不看源碼也可以倒推出很多內(nèi)容)?可以去哪里找解決方案?怎么找到?
- 遷移home頁(yè)配置
- 過(guò)程中縮小home頁(yè)的路徑范圍,隱藏repo A中的訪(fǎng)問(wèn)路徑,僅透出待遷移的路徑,提高查找效率
- 遷移過(guò)程記錄(測(cè)試數(shù)據(jù)及路徑等,方便交叉測(cè)試和QA回歸)
- 覆蓋度自測(cè)。一個(gè)頁(yè)面中多業(yè)務(wù)邏輯的情況,后續(xù)需要對(duì)各路徑進(jìn)行足夠自測(cè)
- 遷移過(guò)程中目錄和文件結(jié)構(gòu)的設(shè)計(jì)與變化路徑(重要)
2.3.3 集成repo A、repo B、repo C重構(gòu)分支代碼
- repo B 中的頁(yè)面遷移到 repo A 中,如用 chore-repoB 分支
- repo C 中的頁(yè)面遷移到 repo A 中,如用 chore-repoC 分支
- 將repo A master分支 和 chore-repoB、chore-repoC 合并并解決沖突,合并分支記為chore-repoA-repoB-repoC,此時(shí)該分支僅有 V1的代碼,各個(gè)倉(cāng)庫(kù)當(dāng)前版本的迭代功能和及上個(gè)版本的hotfix還未被合并入該分支。
2.3.4 集成repo A、repo B、repo C中迭代分支代碼
主版本日前一天下午各個(gè)倉(cāng)庫(kù)中的迭代功能基本穩(wěn)定,bug已經(jīng)收斂。此時(shí)可以將該各個(gè)倉(cāng)庫(kù)的各個(gè)開(kāi)發(fā)本地的分支 feat-foo、feat-bar 等匯總成一個(gè) pre-release-temp 分支(已含有了master上的hotfix),即 pre-release-temp 分支 是 V1.*、V2 的匯總,將該分支的 增量commits合成一個(gè)commit 獲取 V1.*、V2影響到的文件變更。人為將這些變更同步到 repo A chore-repoA-repoB-repoC分支上。
2.3.5 集成三個(gè)倉(cāng)庫(kù)業(yè)務(wù)代碼與SSR代碼
社區(qū)C端SSR改造方案確定后,新啟了一個(gè) A-SSR 倉(cāng)庫(kù)。使用SSR POC的框架內(nèi)容對(duì) A-SSR 倉(cāng)庫(kù)進(jìn)行初始化,再將 repo A中chore-repoA-repoB-repoC 中的代碼遷移到該倉(cāng)庫(kù)中。遇到的問(wèn)題:POC中已對(duì)原 repo A中的部分模塊做了SSR轉(zhuǎn)換,遷移新代碼到該倉(cāng)庫(kù)中注意文件覆蓋代碼丟失,用cp然后git diff及人為check多變更源的文件后再提交。
待版本日中再將近1天 各倉(cāng)庫(kù)產(chǎn)生的bugfix同步到 A-SSR 倉(cāng)庫(kù),確保代碼無(wú)丟失。
3. 項(xiàng)目推進(jìn)之外部協(xié)同
3.1 測(cè)試
較大范圍的重構(gòu)需要保證充分測(cè)試,考慮到占用的測(cè)試資源情況,盡可能提前和測(cè)試leader溝通資源需求。另外,移測(cè)前前端內(nèi)部盡量充分自測(cè)。
3.2 運(yùn)維
提前計(jì)劃好 頁(yè)面重定向方案(將最終的跨倉(cāng)庫(kù)/應(yīng)用遷移的頁(yè)面重定向),注意運(yùn)維側(cè)變更的影響,一旦做了變更,相關(guān)的在對(duì)應(yīng)的測(cè)試環(huán)境就不可用了(QA回歸需要時(shí)間,該過(guò)程中如果重定向啟用了會(huì)影響該環(huán)境上相應(yīng)頁(yè)面的使用)。
3.3 遇到的問(wèn)題
在開(kāi)始規(guī)劃及啟動(dòng)重構(gòu)時(shí),團(tuán)隊(duì)沒(méi)有人對(duì)涉及的所有三個(gè)C端倉(cāng)庫(kù)足夠熟悉。遷移到第二個(gè)頁(yè)時(shí),發(fā)現(xiàn)有頁(yè)面是沒(méi)有線(xiàn)上流量的 dead code時(shí),重新溝通客戶(hù)端及運(yùn)維等同學(xué),最終通過(guò)查詢(xún)阿里云sls日志縮小遷移范圍,減少了近一半的工作量。過(guò)程中遇到的各種技術(shù)問(wèn)題,還是需要平時(shí)多做積累。
4. 總結(jié)
復(fù)雜項(xiàng)目的重構(gòu)對(duì)研發(fā)的基礎(chǔ)、經(jīng)驗(yàn)、規(guī)范和各方協(xié)同有一定要求。開(kāi)始時(shí)可以多讀幾遍《重構(gòu)》基礎(chǔ)的打好了,逐漸著手代碼模塊、簡(jiǎn)單項(xiàng)目、復(fù)雜項(xiàng)目、跨團(tuán)隊(duì)復(fù)雜項(xiàng)目等的重構(gòu),累計(jì)經(jīng)驗(yàn)。事前做好規(guī)劃(技術(shù)側(cè)整體方案、技術(shù)方面的疑難病癥提前預(yù)估、整體推進(jìn)計(jì)劃、相關(guān)方參與等),過(guò)程中思考全面足夠細(xì)心并持續(xù)復(fù)盤(pán)調(diào)整,過(guò)程后做好總結(jié)沉淀。
事前做好設(shè)計(jì)、定期Code Review、過(guò)程中和后續(xù)持續(xù)進(jìn)行重構(gòu)可以讓項(xiàng)目代碼具有更好的可維護(hù)性,團(tuán)隊(duì)保持重構(gòu)的習(xí)慣的同時(shí)不斷積累重構(gòu)經(jīng)驗(yàn),能從整體上提升項(xiàng)目的健康度與可維護(hù)性。重構(gòu)看得見(jiàn)改善是關(guān)鍵,在重構(gòu)中成長(zhǎng),在重構(gòu)中受益,從重構(gòu)中收益。
相關(guān)鏈接:
- https://pughtml.com/
*文/石菲
關(guān)注得物技術(shù)公眾號(hào)~每周一三五晚18:30更新技術(shù)干貨
要是覺(jué)得文章對(duì)你有幫助的話(huà),歡迎評(píng)論轉(zhuǎn)發(fā)點(diǎn)贊~
版權(quán)聲明:本文內(nèi)容由互聯(lián)網(wǎng)用戶(hù)自發(fā)貢獻(xiàn),該文觀(guān)點(diǎn)僅代表作者本人。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如發(fā)現(xiàn)本站有涉嫌抄襲侵權(quán)/違法違規(guī)的內(nèi)容, 請(qǐng)發(fā)送郵件至 舉報(bào),一經(jīng)查實(shí),本站將立刻刪除。