上篇《 不只是前端,后端、产品和测试也需要了解的浏览器知识(一)》介绍了浏览器的基本情况、发展历史以及市场占有率。
本篇文章将介绍浏览器基本原理。在掌握基本原理后,通过技术深入,在研发过程中不断创新,推动产品性能、用户体验的提升,来实现业务的增长,创造可持续的价值。
当用户访问我们的业务系统时,浏览器和服务器之间会进行一系列复杂的交互过程。浏览器整体的导航流程如下:
??
以下是用户从输入 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小时内与您取得联系。