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