上篇《 不只是前端,后端、產(chǎn)品和測(cè)試也需要了解的瀏覽器知識(shí)(一)》介紹了瀏覽器的基本情況、發(fā)展歷史以及市場(chǎng)占有率。
本篇文章將介紹瀏覽器基本原理。在掌握基本原理后,通過技術(shù)深入,在研發(fā)過程中不斷創(chuàng)新,推動(dòng)產(chǎn)品性能、用戶體驗(yàn)的提升,來實(shí)現(xiàn)業(yè)務(wù)的增長(zhǎng),創(chuàng)造可持續(xù)的價(jià)值。
當(dāng)用戶訪問我們的業(yè)務(wù)系統(tǒng)時(shí),瀏覽器和服務(wù)器之間會(huì)進(jìn)行一系列復(fù)雜的交互過程。瀏覽器整體的導(dǎo)航流程如下:
??
以下是用戶從輸入 URL 到看到業(yè)務(wù)系統(tǒng)頁(yè)面的詳細(xì)步驟:
用戶在瀏覽器地址欄中輸入業(yè)務(wù)系統(tǒng)的 URL,例如 https://www.businesssystem.com,并按下回車鍵。
瀏覽器解析輸入的 URL,確定協(xié)議(如 HTTPS)、主機(jī)名(如 www.businesssystem.com)、端口號(hào)(如果有)、路徑、查詢參數(shù)等。
??
瀏覽器需要將主機(jī)名轉(zhuǎn)換為 IP 地址。這個(gè)過程稱為 DNS 解析,通常包括以下步驟:
?瀏覽器首先檢查本地 DNS 緩存,看看是否有對(duì)應(yīng)的 IP 地址。
?如果本地緩存中沒有,瀏覽器會(huì)向操作系統(tǒng)查詢。
?操作系統(tǒng)會(huì)檢查自己的緩存,并可能向本地的 DNS 服務(wù)器發(fā)出請(qǐng)求。
?本地 DNS 服務(wù)器可能會(huì)遞歸查詢其他 DNS 服務(wù)器,直到找到對(duì)應(yīng)的 IP 地址。
一旦獲得了 IP 地址,瀏覽器會(huì)通過 TCP/IP 協(xié)議與服務(wù)器建立連接。對(duì)于 HTTPS,瀏覽器還會(huì)進(jìn)行 SSL/TLS 握手,以建立安全連接。流程如下
??
連接建立后,瀏覽器會(huì)構(gòu)建一個(gè) HTTP 請(qǐng)求并發(fā)送給服務(wù)器。請(qǐng)求包括請(qǐng)求行(例如 GET /index.html HTTP/1.1)、請(qǐng)求頭(如 User-Agent、Accept 等)以及可能的請(qǐng)求體(對(duì)于 POST 請(qǐng)求)。
服務(wù)器接收到請(qǐng)求后,會(huì)根據(jù)請(qǐng)求的內(nèi)容進(jìn)行處理:
?服務(wù)器解析請(qǐng)求,確定所需的資源(如 HTML 文件、圖片、數(shù)據(jù)等)。
?服務(wù)器可能需要與后端數(shù)據(jù)庫(kù)或其他服務(wù)進(jìn)行交互,以生成響應(yīng)內(nèi)容。
?服務(wù)器構(gòu)建 HTTP 響應(yīng),包括狀態(tài)行(如 HTTP/1.1 200 OK)、響應(yīng)頭(如 Content-Type、Content-Length 等)和響應(yīng)體(實(shí)際的頁(yè)面內(nèi)容)。
服務(wù)器將構(gòu)建好的 HTTP 響應(yīng)發(fā)送回瀏覽器。
瀏覽器接收到服務(wù)器的響應(yīng)后,會(huì)根據(jù)響應(yīng)頭的信息處理響應(yīng)體:
?如果響應(yīng)是重定向(如 301 或 302),瀏覽器會(huì)根據(jù) Location 頭再次發(fā)起請(qǐng)求。
?如果響應(yīng)包含壓縮內(nèi)容(如 gzip),瀏覽器會(huì)解壓縮。
?瀏覽器會(huì)根據(jù) Content-Type 頭確定如何處理響應(yīng)體(如 HTML、CSS、JavaScript、圖片等)。
發(fā)送請(qǐng)求和接受響應(yīng)流程如下:
??
瀏覽器開始解析 HTML 文檔,構(gòu)建 DOM 樹。解析過程中,瀏覽器會(huì)處理各種 HTML 標(biāo)簽,并根據(jù)需要發(fā)起其他請(qǐng)求(如 CSS、JavaScript、圖片等)。
?CSS:瀏覽器解析 CSS 文件并構(gòu)建 CSSOM 樹,與 DOM 樹結(jié)合形成渲染樹。
?JavaScript:瀏覽器解析和執(zhí)行 JavaScript 代碼,可能會(huì)修改 DOM 樹或 CSSOM 樹。
?圖片和其他資源:瀏覽器會(huì)異步加載這些資源,并在加載完成后進(jìn)行渲染。
瀏覽器根據(jù)渲染樹計(jì)算每個(gè)元素的布局(位置和大小),并將頁(yè)面繪制到屏幕上。這個(gè)過程可能會(huì)涉及多次重繪和重排(reflow/repaint),尤其是在 JavaScript 修改 DOM 或 CSS 的情況下。
頁(yè)面渲染流程如下:
??
頁(yè)面加載完成后,用戶可以與頁(yè)面進(jìn)行交互。瀏覽器會(huì)響應(yīng)用戶的操作(如點(diǎn)擊、輸入等),并可能通過 JavaScript 動(dòng)態(tài)更新頁(yè)面內(nèi)容。
業(yè)務(wù)系統(tǒng)的呈現(xiàn)過程主要是:URL解析、與服務(wù)器建立連接、服務(wù)器處理請(qǐng)求并返回響應(yīng)、下載和解析響應(yīng)、頁(yè)面渲染。
?DNS 預(yù)解析(DNS Prefetching):瀏覽器在用戶點(diǎn)擊鏈接之前,提前解析該鏈接的域名,從而減少等待時(shí)間。
<link rel="dns-prefetch" href="//example.com">
合理設(shè)置 DNS 記錄的 TTL(Time-To-Live),使得 DNS 記錄可以在客戶端和中間緩存服務(wù)器上保存適當(dāng)?shù)臅r(shí)間,減少重復(fù)解析請(qǐng)求。
?對(duì)于不經(jīng)常變化的記錄,可以設(shè)置較長(zhǎng)的 TTL 值(如 24 小時(shí))。
?對(duì)于經(jīng)常變化的記錄,可以設(shè)置較短的 TTL 值(如幾分鐘到幾小時(shí))。
?負(fù)載均衡:使用 DNS 負(fù)載均衡技術(shù),將流量分配到多臺(tái)服務(wù)器上,防止單點(diǎn)故障。
?冗余配置:配置多個(gè)權(quán)威 DNS 服務(wù)器,確保在一個(gè)服務(wù)器故障時(shí),其他服務(wù)器可以繼續(xù)提供解析服務(wù)。
?合并資源:盡量將資源放在同一個(gè)域名下,減少跨域名的 DNS 查詢次數(shù)。
?減少外部資源:盡量減少頁(yè)面中引用的外部資源(如第三方腳本和樣式),以減少額外的 DNS 查詢。
?使用合適的請(qǐng)求方法:確保使用正確的 HTTP 方法(GET、POST、PUT、DELETE 等)來表示操作的意圖。例如,使用 GET 方法獲取數(shù)據(jù),使用 POST 方法提交數(shù)據(jù)。
?避免不必要的請(qǐng)求:合并請(qǐng)求,減少頁(yè)面中的請(qǐng)求次數(shù)。例如,CSS 和 JavaScript 文件可以合并,圖像可以使用精靈圖(sprite)。
?正確使用狀態(tài)碼:確保服務(wù)器返回正確的 HTTP 狀態(tài)碼。例如,200 表示成功,404 表示資源未找到,500 表示服務(wù)器錯(cuò)誤。
?重定向優(yōu)化:減少重定向次數(shù),避免不必要的 301 或 302 重定向。
?壓縮傳輸內(nèi)容:使用 Gzip 或 Brotli 壓縮傳輸內(nèi)容,減少傳輸數(shù)據(jù)量。
?緩存控制:使用緩存控制頭(如 Cache-Control、Expires)來緩存靜態(tài)資源,減少重復(fù)請(qǐng)求。
?內(nèi)容安全策略(CSP):設(shè)置內(nèi)容安全策略頭,防止跨站腳本攻擊(XSS)。
?減少頭部大小:刪除不必要的請(qǐng)求和響應(yīng)頭,減少頭部大小,加快傳輸速度。
a. 多路復(fù)用
?啟用 HTTP/2 或 HTTP/3:這些協(xié)議支持多路復(fù)用,可以在一個(gè) TCP 連接中同時(shí)發(fā)送多個(gè)請(qǐng)求和響應(yīng),減少延遲。
?減少域名分片:HTTP/2 和 HTTP/3 中,多路復(fù)用使得域名分片(將資源分布到多個(gè)子域名)不再必要,反而可能降低性能。
b. 頭部壓縮
?使用 HPACK(HTTP/2)或 QPACK(HTTP/3)頭部壓縮:這些協(xié)議支持頭部壓縮,減少傳輸?shù)臄?shù)據(jù)量。
c. 減少延遲
?使用優(yōu)先級(jí)和依賴:HTTP/2 和 HTTP/3 支持請(qǐng)求優(yōu)先級(jí)和依賴,確保關(guān)鍵資源優(yōu)先加載。
?啟用 QUIC 協(xié)議(HTTP/3):QUIC 協(xié)議基于 UDP,減少了連接建立的延遲,提供更快的傳輸速度。
?使用 CDN:將靜態(tài)資源分發(fā)到全球各地的節(jié)點(diǎn),減少用戶訪問的延遲。
?邊緣計(jì)算:利用 CDN 的邊緣計(jì)算能力,在靠近用戶的節(jié)點(diǎn)上處理部分邏輯,減少服務(wù)器負(fù)載。
?靜態(tài)資源托管:將靜態(tài)資源(如圖像、CSS、JavaScript)托管在 CDN 上,減少網(wǎng)絡(luò)延遲,加快加載速度。
??
a. 減少 DOM 復(fù)雜度
?簡(jiǎn)化 HTML 結(jié)構(gòu):減少嵌套層級(jí),避免過度復(fù)雜的 DOM 結(jié)構(gòu)。
?刪除不必要的元素:移除無用的 HTML 標(biāo)簽和注釋。
b. 延遲加載非關(guān)鍵內(nèi)容
?使用 defer 和 async:對(duì)非關(guān)鍵 JavaScript 文件使用 defer 或 async 屬性,避免阻塞頁(yè)面渲染。
?懶加載圖像和視頻:使用 loading="lazy" 屬性或 JavaScript 實(shí)現(xiàn)懶加載,延遲加載視口外的圖像和視頻。
a. 減少 CSS 文件大小
?壓縮 CSS 文件:使用工具(如 CSSNano、CleanCSS)壓縮 CSS 文件,減少文件大小。
?移除未使用的 CSS:使用工具(如 PurgeCSS)移除未使用的 CSS 規(guī)則。
b. 優(yōu)化 CSS 加載
?使用外部樣式表:將 CSS 放在外部樣式表中,而不是內(nèi)聯(lián)樣式,便于緩存和管理。
?放置 CSS 在 <head> 中: 確保 CSS 文件在 <head> 中加載,以便盡快渲染頁(yè)面。
?避免 CSS 阻塞渲染:將關(guān)鍵 CSS 內(nèi)聯(lián)到 HTML 中,非關(guān)鍵 CSS 異步加載。
a. 減少 JavaScript 文件大小
?壓縮和混淆:使用工具(如 UglifyJS、Terser)壓縮和混淆 JavaScript 文件,減少文件大小。
?移除未使用的代碼:使用工具(如 Webpack 的 Tree Shaking)移除未使用的代碼。
b. 優(yōu)化 JavaScript 加載
?分離關(guān)鍵和非關(guān)鍵腳本:將關(guān)鍵腳本放在 <head> 中,非關(guān)鍵腳本放在頁(yè)面底部或使用 defer 和 async。
?代碼分割:使用 Webpack 等工具進(jìn)行代碼分割,按需加載模塊。
c. 優(yōu)化 JavaScript 執(zhí)行
?減少重排和重繪:避免頻繁操作 DOM,使用文檔片段(Document Fragment)或虛擬 DOM 技術(shù)。
?使用節(jié)流和防抖:對(duì)高頻率事件(如滾動(dòng)、輸入)使用節(jié)流(throttle)和防抖(debounce)技術(shù),減少不必要的函數(shù)調(diào)用。
?減少 JavaScript 阻塞:避免長(zhǎng)時(shí)間運(yùn)行的 JavaScript 任務(wù),使用 Web Workers 將復(fù)雜計(jì)算移到后臺(tái)線程。
a. 減少圖像文件大小
?壓縮圖像:使用工具(如 ImageOptim、TinyPNG)壓縮圖像文件,減少文件大小。
?選擇合適的格式:根據(jù)圖像內(nèi)容選擇合適的格式(如 JPEG、PNG、WebP),WebP 通常比 JPEG 和 PNG 更小。
b. 優(yōu)化圖像加載
?使用響應(yīng)式圖像:使用 srcset 和 sizes 屬性提供不同分辨率的圖像,適應(yīng)不同設(shè)備。
?懶加載圖像:使用 loading="lazy" 屬性或 JavaScript 實(shí)現(xiàn)圖像懶加載。
a. 優(yōu)化字體加載
?使用字體顯示策略:使用 font-display 屬性控制字體加載行為,避免字體閃爍(FOIT)和不可見文本(FOUT)。
?減少字體文件大小:使用子集化工具(如 Google Fonts 的子集化功能)只加載需要的字符集,減少字體文件大小。
在實(shí)際業(yè)務(wù)中我們需要針對(duì)頁(yè)面呈現(xiàn)過程中的每一個(gè)節(jié)點(diǎn),去制定不同的優(yōu)化策略。
本文主要介紹了業(yè)務(wù)系統(tǒng)呈現(xiàn)給用戶所經(jīng)歷的各個(gè)節(jié)點(diǎn),以及作為技術(shù)人能在各節(jié)點(diǎn)中進(jìn)行優(yōu)化的點(diǎn), 通過這些技術(shù)優(yōu)化點(diǎn),在研發(fā)過程中不斷創(chuàng)新,推動(dòng)產(chǎn)品性能、用戶體驗(yàn)的提升,來實(shí)現(xiàn)業(yè)務(wù)的增長(zhǎng),創(chuàng)造可持續(xù)的價(jià)值。
生 CSS 嵌套(Native CSS nesting)已經(jīng)被所有現(xiàn)代桌面瀏覽器所支持!,但是請(qǐng)注意,移動(dòng)端瀏覽器支持的還很有限。
原生 CSS 嵌套可以像 SASS、LESS 預(yù)處理器一樣,將相關(guān)的選擇器組合在一起,從而減少需要編寫的規(guī)則數(shù)量,它可以節(jié)省打字時(shí)間,并使語法更易于閱讀和維護(hù)。您可以將選擇器嵌套到任意深度,但要小心不要超過兩層或三層。嵌套深度沒有技術(shù)限制,但它會(huì)使代碼更難以閱讀,并且生成的 CSS 可能會(huì)變得不必要的冗長(zhǎng)。
.button {
background-color: red;
&.warning {
background-color: blue;
}
& .icon {
width: 1rem;
height: 1rem;
}
}
雖然原生 CSS 嵌套語法在過去幾年中不斷發(fā)展,使大多數(shù) Web 開發(fā)人員感到滿意,但不要指望所有 SCSS 代碼都能像您期望的那樣直接工作。
您可以將任何選擇器嵌套在另一個(gè)選擇器中,但它必須以符號(hào)開頭,例如 &, .(類選擇器)、#(ID選擇器)、@(對(duì)于媒體查詢)、:、::、+、 ~、 > 或 [。換句話說,它不能是對(duì) HTML 元素的直接引用。下面的代碼是無效的,不會(huì)對(duì) input 元素選擇器進(jìn)行解析:
.parent {
color: red;
input {
margin: 1em;
}
}
/* Invalid, because "input" is an identifier. */
解決此問題的最簡(jiǎn)單方法是使用與號(hào) ( &),它以與 Sass 相同的方式引用當(dāng)前選擇器。
.parent {
color: red;
& input {
margin: 1em;
}
/* use pseudo-elements and pseudo-classes */
&::after {}
&:hover {}
&:target {}
}
/* valid, no longer starts with an identifier */
或者,您可以使用其中之一:
它們都可以在這個(gè)簡(jiǎn)單的示例中工作,但是稍后您可能會(huì)遇到更復(fù)雜的樣式表的特異性問題。
它還&允許您在父選擇器上定位偽元素和偽類。例如:
p.my-element {
&::after {}
&:hover {}
&:target {}
}
請(qǐng)注意,& 可以在選擇器中的任何位置使用。例如:
.child1 {
.parent3 & {
color: red;
}
}
這將轉(zhuǎn)換為以下非嵌套語法:
.parent3 .child1 { color: red; }
您甚至可以在選擇器中使用多個(gè) & 符號(hào):
ul {
& li & {
color: blue;
}
}
這將以嵌套 <ul> 元素 ( ul li ul) 為目標(biāo),但如果您想保持理智,我建議不要使用它!
嵌套媒體查詢示例:
p {
color: cyan;
@media (min-width: 800px) {
color: purple;
}
}
原生 CSS 嵌套將父選擇器包裝在 :is() 中,這可能會(huì)導(dǎo)致與 Sass 輸出的差異,比如以下嵌套代碼:
.parent1, #parent2 {
.child1 {
}
}
當(dāng)它在瀏覽器中解析時(shí),它實(shí)際上變成以下內(nèi)容:
:is(.parent1, #parent2) .child1 {
}
Sass 將相同的代碼編譯為:
.parent1 .child1,
#parent2 .child1 {
}
您可能還會(huì)遇到一個(gè)更微妙的問題。考慮一下:
.parent .child {
.grandparent & {}
}
原生 CSS 等效項(xiàng)是:
.grandparent :is(.parent .child) {}
這與以下錯(cuò)誤排序的 HTML 元素匹配:
<div class="parent">
<div class="grandparent">
<div class="child">MATCH</div>
</div>
</div>
MATCH變得有樣式是因?yàn)?CSS 解析器執(zhí)行以下操作:
它會(huì)查找所有元素,其所屬類的child祖先也parent為DOM 層次結(jié)構(gòu)中的任何點(diǎn)。
找到包含MATCH的元素后,解析器會(huì)grandparent在 DOM 層次結(jié)構(gòu)中的任何位置再次檢查它是否具有 — 的祖先。它找到一個(gè)并相應(yīng)地設(shè)置該元素的樣式。
Sass 中的情況并非如此,它編譯為:
.grandparent .parent .child {} 上面的 HTML 沒有樣式化,因?yàn)樵仡惒蛔裱瓏?yán)格的grandparent、parent、 和child順序。
Sass 使用字符串替換,因此如下所示的聲明是有效的,并且與類的任何元素相匹配 .btn-primary:
.btn {
&-primary {
color: blue;
}
}
但是原生 CSS 嵌套會(huì)忽略&-space選擇器。
從短期來看,現(xiàn)有的 CSS 預(yù)處理器仍然至關(guān)重要。Sass 開發(fā)團(tuán)隊(duì)宣布,他們將支持 .css 文件中的原生 CSS 嵌套,并按原樣輸出代碼。他們將一如既往地編譯嵌套 SCSS 代碼,以避免破壞現(xiàn)有代碼庫(kù),但當(dāng)全球?yàn)g覽器支持率達(dá)到 98% 時(shí),他們將開始輸出 :is() 選擇器。
我猜想,PostCSS 插件等預(yù)處理器目前會(huì)擴(kuò)展嵌套代碼,但隨著瀏覽器支持的普及,就會(huì)取消這一功能。當(dāng)然,使用預(yù)處理器還有其他很好的理由,比如將部分代碼捆綁到一個(gè)文件中,以及對(duì)代碼進(jìn)行精簡(jiǎn)。但如果嵌套是你唯一需要的功能,你當(dāng)然可以考慮在較小的項(xiàng)目中使用原生 CSS。
CSS 嵌套是最有用、最實(shí)用的預(yù)處理器功能之一。瀏覽器供應(yīng)商努力創(chuàng)造了一個(gè)與 CSS 非常相似的原生 CSS 版本,以滿足網(wǎng)絡(luò)開發(fā)人員的需求。雖然兩者之間存在細(xì)微差別,而且在使用(過于)復(fù)雜的選擇器時(shí)可能會(huì)遇到不尋常的特殊性問題,但很少有代碼庫(kù)需要進(jìn)行徹底修改。
原生嵌套可能會(huì)讓你重新考慮是否需要 CSS 預(yù)處理器,但它們?nèi)阅芴峁┢渌锰帯ass 和類似工具仍然是大多數(shù)開發(fā)者工具包的重要組成部分。
峰(楚梟) 阿里開發(fā)者 2023-05-23 09:01 發(fā)表于浙江
阿里妹導(dǎo)讀
在日常開發(fā)中,遇到非常難以維護(hù)的頁(yè)面是常態(tài),相信不少同學(xué)也為此苦惱過,筆者在業(yè)務(wù)開發(fā)中總結(jié)了些經(jīng)驗(yàn)希望對(duì)大家有所啟發(fā)。(后臺(tái)回復(fù)大數(shù)據(jù)即可獲得《大數(shù)據(jù)&AI實(shí)戰(zhàn)派》電子書)
背景
在日常開發(fā)中,遇到非常難以維護(hù)的頁(yè)面是常態(tài),相信不少同學(xué)也為此苦惱過,筆者在業(yè)務(wù)開發(fā)中總結(jié)了些經(jīng)驗(yàn)希望對(duì)大家有所啟發(fā)。下圖是一個(gè)較為復(fù)雜的詳情頁(yè)、表單頁(yè),我截取了其中一小部分作為示例:
隨著需求不斷迭代,這個(gè)頁(yè)面的代碼變得越來越復(fù)雜,代碼達(dá)到幾千行,html 標(biāo)簽嵌套層級(jí)非常深,每次想在正確的節(jié)點(diǎn)改東西、加元素都非常費(fèi)眼睛;每次想修改、疊加業(yè)務(wù)邏輯,看到一堆 useEffect、useState、useRef 令人望而卻步。于是決定重構(gòu)以改變現(xiàn)狀。
如何重構(gòu),以拆解復(fù)雜頁(yè)面
如何重構(gòu)一個(gè)復(fù)雜前端頁(yè)面?筆者平常主要寫后端,實(shí)際工程中后端代碼的腐化很多都來源于 if-else 不斷疊加,要重構(gòu)一般分幾個(gè)層次看:
大的思路如此,具體場(chǎng)景各有各的特殊性,需要靈活應(yīng)對(duì),這里也不過多展開。總之,后端的復(fù)雜度和各個(gè)場(chǎng)景的業(yè)務(wù)邏輯息息相關(guān),垂直縱深很大,但前端呢?私以為前端雖然也有業(yè)務(wù)邏輯但不深,它的復(fù)雜不是來源于垂直縱深,而是水平堆積。一個(gè)頁(yè)面的內(nèi)容經(jīng)常又多又雜,有詳情、有表單、各種區(qū)塊、不同標(biāo)簽頁(yè),里面的內(nèi)容我不贅述。那么重構(gòu)的方向呼之欲出:使用組合的思想拆分水平堆積的業(yè)務(wù)邏輯塊。具體到 React,其實(shí)就是拆解業(yè)務(wù)、封裝組件,是一個(gè)組件化的過程。
組件化
其實(shí)前端整個(gè) React App 說白了可以抽象成一個(gè)組件樹,如圖:
筆者習(xí)慣將組件分成:基礎(chǔ)組件和區(qū)塊組件。
按照基礎(chǔ)組件和區(qū)塊組件的劃分,我開始重構(gòu)上圖詳情頁(yè)。
組件封裝實(shí)踐
我將頁(yè)面上的展示內(nèi)容按照業(yè)務(wù)塊進(jìn)行了劃分,自頂向上對(duì)業(yè)務(wù)區(qū)塊進(jìn)行了重新的定義,如圖:
劃分好了就開始封裝,列舉幾個(gè)組件的封裝示例。
基礎(chǔ)組件:AliTalk IM
接收方 IM 身份的 id 作為入?yún)ⅲ祷?IM 展示組件,點(diǎn)擊 icon 則喚起聊天彈窗進(jìn)行聊天操作,完成后可關(guān)閉彈窗。至于初始化聊天框、銷毀聊天框的邏輯,以及如何進(jìn)行聊天,應(yīng)該在組件內(nèi)封裝好,外部業(yè)務(wù)不關(guān)心這些,主要代碼:
type ChatProps={
uid?: string;
};
const Chat: FC<ChatProps>=(data: ChatProps)=> {
const [showChat, setShowChat]=useState(true);
useEffect(()=> {
console.log('init Alitalk: ' + data.uid);
return ()=> {
console.log('destroy Alitalk: ' + data.uid);
setShowChat(false);
const aliTalkMessageBox=document.getElementsByClassName('weblite-iframe');
for (let i=0; i < aliTalkMessageBox.length; i++) {
const item=aliTalkMessageBox[i];
item.remove();
}
};
}, []);
return (
<div>
{showChat && (
<Alitalk uid={data.uid} pid={'xx'} bizType={1} bizId={'xx'} >
<img width={24} height={24}
src={
'https://img.alicdn.com/imgextra/i2/O1CN01acXzMG1d5JsurHGVR_!!6000000003684-2-tps-200-200.png'
} />
<span style={{ marginLeft: '5px', color: '#FF6600' }}>chat now</span>
</Alitalk>
)}
</div>
);
};
export default Chat;
直接引入 <Chat> 標(biāo)簽,一行代碼即可:
<Descriptions style={{ marginBottom: 24 }} title="買家信息">
<Descriptions.Item label="買家旺旺">
<Chat uid={detailData?.data?.buyerAliTalkId} />
</Descriptions.Item>
</Descriptions>
區(qū)塊組件:操作欄行動(dòng)點(diǎn)彈窗
以移交服務(wù)單為例,點(diǎn)擊按鈕則喚起轉(zhuǎn)交表單彈窗,填完表單后提交則發(fā)起請(qǐng)求,完成后自動(dòng)關(guān)閉彈窗。表單提交的邏輯,操作欄展示區(qū)塊并不關(guān)心,封裝一個(gè) TransferOrderModalForm 組件內(nèi)聚這些業(yè)務(wù)邏輯即可。
<Fragment>
<Button.Group>
<EstimatedQuotationModalForm orderId={detailData?.id} />
<DomesticWarehouseReceivingModalForm orderId={detailData?.id} />
<OfficialQuotationModalForm orderId={detailData?.id} />
<MarkOrderPaidModalForm orderId={detailData?.id} />
<MarkOrderExceptionModalForm orderId={detailData?.id} />
<MarkOrderClosedModalForm orderId={detailData?.id} />
{/* 移交服務(wù)單 ModalForm */}
<TransferOrderModalForm orderId={detailData?.id} />
</Button.Group>
</Fragment>
按照組件拆分后,主頁(yè)面的代碼行數(shù)從幾千行降低到 200 行,主頁(yè)面僅僅只做了對(duì)其他組件的引用和頁(yè)面編排,其引用的業(yè)務(wù)區(qū)塊組件如果夠復(fù)雜,還能繼續(xù)再次拆分組件,整個(gè)頁(yè)面就成了一個(gè)掛載的組件樹,但每個(gè)區(qū)塊都只關(guān)心自己的業(yè)務(wù)抽象層次,符合 SLAP 原則。
組件封裝的思考
關(guān)于組件設(shè)計(jì)思想
基礎(chǔ)組件應(yīng)該做成原子能力,不要陷入業(yè)務(wù)場(chǎng)景中,參數(shù)要設(shè)計(jì)得普適性強(qiáng)一點(diǎn),這樣設(shè)計(jì)出來的組件復(fù)用性強(qiáng),比如聊天組件、獲取當(dāng)前登錄用戶組件、鑒權(quán)組件等等,都符合這種情形。
而業(yè)務(wù)區(qū)塊組件恰恰相反,完全沒必要考慮復(fù)用性,目標(biāo)就是把不同業(yè)務(wù)抽象層次進(jìn)行拆分、隔離,使得整個(gè)業(yè)務(wù)層層遞進(jìn),每個(gè)層次都只關(guān)心自己應(yīng)該關(guān)心的業(yè)務(wù),這樣設(shè)計(jì)出來的組件高內(nèi)聚、易讀、易維護(hù),當(dāng)然,如果能復(fù)用那更好,算是增值收益了,但這不是目標(biāo)。
業(yè)務(wù)區(qū)塊組件應(yīng)該自頂向下設(shè)計(jì),開始的時(shí)候應(yīng)該設(shè)計(jì)得粗粒度一點(diǎn),隨著業(yè)務(wù)不斷的迭代可以慢慢下沉,而一開始就想設(shè)計(jì)精細(xì),想要一步到位,反而會(huì)隨著后續(xù)的業(yè)務(wù)迭代不斷要打破進(jìn)行調(diào)整,喪失了靈活性。
關(guān)于前后端思想上的融會(huì)貫通
雖然前端的組件和后端的類要怎么設(shè)計(jì)、怎么實(shí)現(xiàn),看起來區(qū)別很大,但咱們剖析表象看本質(zhì),思想其實(shí)是一脈相承的,舉幾個(gè)例子:
戰(zhàn)術(shù)上,React 現(xiàn)在推行的是函數(shù)式組件,給一組入?yún)ⅲ祷卣故驹兀?jiǎn)單的輸入輸出無副作用;后端也一樣,要盡量避免一個(gè)對(duì)象參數(shù)在不同的方法不同的節(jié)點(diǎn)被改來改去,最后改成了啥都不知道,不利于維護(hù),也容易出 bug,所以很多 API 比如 Stream.map() 的設(shè)計(jì)都提倡不要把對(duì)象改來改去,而應(yīng)該干凈利落的使用純函數(shù)。
戰(zhàn)略上,SLAP 單一抽象層次原則從來都不針對(duì)是前端還是后端,前端組件也好,后端類也好,都要搞清楚每個(gè)業(yè)務(wù)層次關(guān)心的核心要素是什么,把不該關(guān)心的東西丟給其他業(yè)務(wù)層次完成,不要把編碼變成了一個(gè)翻譯業(yè)務(wù)需求的動(dòng)作,而應(yīng)該像畫家作畫一樣,先構(gòu)圖再落筆。
《黑客與畫家》中描繪了黑客與畫家的諸多相同點(diǎn),“畫作永遠(yuǎn)沒有完工的一天,你只是不再畫下去而已”。希望追求卓越的你能始終保持那份對(duì)設(shè)計(jì)的熱忱。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。