如何构建一个博客

Orange
Orange

1 网站怎样通过网络到达浏览器

1.1 浏览器与服务器的分工

你在地址栏输入博客地址后,浏览器怎样得到内容,并把它显示成网页?

当你打开浏览器,在地址栏输入https://www.orangelab.top并按下回车时,浏览器会向网站所在的服务器发送请求,希望取得首页的内容。 服务器接收到请求后,执行相应的处理程序,并将处理结果返回给浏览器。浏览器再解析这些内容,最终把文字、图片和页面布局显示在屏幕上。

在这个过程中,浏览器与服务器承担着不同的任务。浏览器主要负责与用户交互和显示页面,服务器主要负责处理请求、读取数据和生成响应。 要理解一个网站如何运行,首先需要把这两部分工作区分清楚。

1.1.1 地址栏访问与静态文件访问

访问一个网页,并不一定是在下载服务器上已经写好的 HTML 文件。 对于本项目的博客首页,服务器需要先查询文章数据,再根据模板生成页面。因此,首页的内容是在处理请求的过程中生成的。

以访问博客首页为例,其主要过程如下:

  1. 浏览器发出请求。访问网站首页时,请求路径为 /,表示希望取得该网站的首页资源。

  2. Flask 匹配路由。服务器根据请求路径,找到负责处理首页请求的 home() 函数,并执行其中的代码。

  3. 查询数据库。函数从 SQLite 数据库中读取已发布文章等数据,为生成首页准备内容。

  4. 渲染模板。服务器把查询结果传入 home.html 模板,执行模板中的变量替换、条件判断和循环,生成 HTML 文本。

  5. 返回并显示页面。服务器把生成的 HTML 作为响应正文返回,浏览器接收后解析页面结构,并继续取得页面需要的其他资源。

但是,仅有 HTML 通常还不足以完整显示一个网页。HTML 主要描述页面中有哪些内容,例如标题、段落、链接和图片位置;CSS 负责控制这些内容的颜色、大小、间距和排列;JavaScript 则可以处理按钮点击、主题切换等交互行为。图片和字体等内容也可能需要单独取得。

因此,浏览器取得首页 HTML 后,还会根据其中引用的资源地址,继续请求样式表、脚本和图片。用户看到的虽然是一张页面,背后却可能对应多条请求。

本项目中,样式和脚本等资源通常通过/static/ 路径访问,上传的图片等文件通过/uploads/ 路径访问。

CSS、JavaScript、图片和字体等文件通常称为静态资源。与需要查询数据并填充模板的首页相比,提供这些资源时,服务器主要返回对应文件的内容。

实际访问时,浏览器还可能复用缓存中的资源,因此并非每次刷新都会重新下载所有文件。

1.1.2 服务器计算与浏览器显示的边界

服务器生成页面内容,浏览器负责解析和显示这些内容。两者运行在不同的环境中,各自拥有自己的内存。

在服务器一侧,Python 程序可以查询数据库、处理业务逻辑,并根据需要检查用户身份和权限;在浏览器一侧,浏览器解析收到的 HTML,建立页面元素之间的结构关系,再结合 CSS 完成布局与绘制,同时执行网页中的 JavaScript。

以首页文章列表为例,home() 查询数据库后,会把结果保存在 Python 变量posts 中。 此时,文章数据仍然位于服务器内存中,浏览器不能直接读取这个变量。随后,服务器通过render_template把文章数据传入首页模板。 首页模板把posts 赋给listing,文章卡片模板再遍历listing,逐篇生成标题、摘要和链接等内容。以上过程都发生在服务器上。

假设其中一篇文章的标题为“我的第一篇博客”,模板中的{post.title} 就会被替换为这段具体文字,并放入相应的 HTML 标签中。浏览器收到的是已经生成好的标题标签。

因此,文章从数据库到屏幕,大致经历了以下转换: 数据库记录先被读取为 Python 中的数据,再由模板转换为 HTML,最后由浏览器解析并显示。

浏览器解析 HTML 后,会建立文档对象模型,简称 DOM。可以把它理解为浏览器在内存中组织页面元素的一棵树。JavaScript 可以修改这棵树中的文字、属性和结构,从而改变当前页面的显示。

不过,修改浏览器中的页面元素,并不等于修改服务器中的数据。例如,在开发者工具中临时改掉文章标题,只会影响当前浏览器里的显示;由于数据库中的标题没有改变,刷新页面后,通常仍会显示服务器重新返回的原标题。

反过来,服务器数据库中的内容发生变化,也不会自动出现在已经打开的页面中。浏览器需要刷新页面、重新发送请求,或者通过网站专门设计的更新机制,才能取得新的内容。

1.2 IP、域名、DNS 与端口

上一节介绍了浏览器与服务器的分工,但浏览器要向服务器发送请求,还需要解决两个问题:应该连接哪个网络地址,以及连接这个地址上的哪个服务。

1.2.1 从域名到服务器地址

IP 地址用于网络中的寻址。浏览器与服务器通信时,需要使用目标 IP 地址建立连接。 对于用户来说,直接记忆一串 IP 地址并不方便,因此网站通常使用域名作为访问入口。 例如,在https://www.orangelab.top中,www.orangelab.top是域名。 域名提供了便于记忆的名称,但浏览器建立网络连接时,仍然需要取得相应的 IP 地址。

DNS(域名系统),负责提供域名相关记录的查询。其中,与访问网站密切相关的是地址记录:A 记录提供 IPv4 地址,AAAA 记录提供 IPv6 地址。 可以先把 DNS 理解为一种查询机制:输入域名,查询可以用于连接的地址。

以访问博客首页为例,在通常的直接访问过程中,可以把域名解析概括为以下步骤:

  1. 浏览器准备访问域名 www.orangelab.top。

  2. 浏览器和操作系统检查是否已有可以使用的解析缓存。如果存在仍然有效的结果,就可能直接复用。

  3. 如果没有可用结果,系统通过配置的 DNS 解析服务查询域名对应的地址。

  4. 取得地址后,浏览器使用相应的 IP 地址和端口尝试建立连接,再进行后续的网站访问。

因此,DNS 查询并不一定在每次刷新页面时都完整执行。浏览器、操作系统和 DNS 解析服务都可能保存缓存,减少重复查询。

域名与 IP 地址并不是简单的一一对应关系。一个域名可以配置多个地址,以满足不同的访问或部署需求;多个域名也可以指向同一个 IP 地址,由服务器根据请求中的域名等信息区分网站。

还需要注意,DNS 主要处理域名,不负责解析整个 URL 的业务含义。例如,对于:

https://www.orangelab.top/articles?page=2

其中只有www.orangelab.top是域名; /articles是请求路径,page=2是查询参数。

DNS 解析成功,也不等于网站一定可以访问。它只说明取得了地址结果,后续仍可能出现其他问题。 因此,排查访问故障时,应先判断域名是否解析到预期地址,再判断连接与网站服务是否正常。

1.2.2 端口与网站服务进程

取得服务器的 IP 地址后,还需要确定应该连接服务器上的哪个网络服务。同一台服务器可以同时运行网站、数据库和其他服务,端口用于区分网络通信的不同入口。

网络服务通常会监听指定的 IP 地址和端口,等待连接到来。系统根据连接使用的网络协议、地址和端口等信息,把通信交给相应的服务程序。因此,描述一个服务的监听位置时,应同时关注地址和端口,而不能只看端口号。

对于通常的 HTTP 和 HTTPS 网站访问,默认端口分别为80 和 443。例如:

http://www.orangelab.top/

https://www.orangelab.top/

在没有显式填写端口时,上面两个地址分别使用默认端口80 和 443。

如果服务使用其他端口,就需要在 URL 中明确写出。例如:

http://127.0.0.1:8000/

其中,127.0.0.1是目标地址,8000是端口,/是请求路径。 这个地址表示通过 HTTP,访问当前机器回环地址上监听 8000 端口的服务。

最后,端口号相同,并不意味着服务一定相同;使用了某个常见端口,也不代表程序自动具备对应协议的能力。 例如,把普通 HTTP 服务的监听端口改为 443,并不会自动使它支持 HTTPS。

如果启动程序时提示端口被占用,应先确认哪个进程正在使用相应的监听地址和端口,再判断是已有服务正在运行,还是其他程序发生了冲突。 端口调整还可能影响代理转发和访问地址,因此不能只修改应用中的数字,而忽略其他配置之间的对应关系。

1.3 URL 与 HTTP 消息

前面介绍了如何通过域名取得服务器地址,以及如何通过端口找到对应的网络服务。 接下来还需要解决两个问题:浏览器希望访问哪个资源,以及浏览器与服务器通过什么形式交换信息。

1.3.1 路径、查询参数与片段标识

URL 不只是一个网站名称,还可以包含协议、主机、端口、路径、查询字符串和片段标识。以如下地址为例:

这个地址可以拆分为以下部分:

协议

https,说明使用 HTTPS 访问。

主机

www.orangelab.top,表示访问的目标域名。

路径

/articles,表示希望访问网站中的文章列表资源。

查询字符串

q=Flask&page=2,为本次访问提供额外参数。

片段标识

main-content,通常用于浏览器在文档内定位相应元素。

这个 URL 没有显式填写端口,因此使用 HTTPS 的默认端口443。

对于 Flask 应用,路径是路由匹配的重要依据。

1.3.1.1 路径决定访问哪个资源

例如,/可以对应首页,/articles可以对应文章列表,而文章详情可以通过带有文章别名的路径来区分。

1.3.1.2 查询参数补充本次请求的条件

查询字符串从问号开始,常见写法为“参数名=参数值”,多个参数通常使用&连接。

这些含义由应用程序约定。浏览器并不知道q一定表示搜索,page一定表示页码;它只是按 URL 发送参数,由服务器代码解释和处理。 在 Flask 中,可以通过request.args读取查询参数。

1.3.1.3 片段标识主要由浏览器处理

片段标识位于井号之后。对于普通网页,浏览器通常会尝试找到具有对应id的元素,并滚动到相应位置。 片段不会作为本次 HTTP 请求的路径或查询字符串发送给服务器。

1.3.1.4 通过程序生成链接

URL 中的字符可能需要编码。例如,参数值本身包含空格、&或#时,不能直接把这些字符与 URL 的结构分隔符混在一起。

1.3.2 GET、POST、请求头、请求体与状态码

浏览器取得 URL 后,需要通过 HTTP 请求表达自己的访问意图。服务器处理请求后,再通过 HTTP 响应返回结果。一次请求与响应构成了双方的一次信息交换。

下面使用 HTTP/1.1 的文本形式说明消息结构。 实际浏览器也可能使用 HTTP/2 或 HTTP/3,它们的传输表示不同,但仍然具有方法、请求目标、头字段、状态码和正文等概念。

1.3.2.1 请求由哪些部分组成

一个简化的 GET 请求可以写成:

GET /articles?q=Flask&page=2 HTTP/1.1

Host: example.test

Accept: text/html

第一行是请求行,其中:

  • GET 是请求方法,表示本次请求的操作语义。

  • /articles?q=Flask&page=2 是请求目标,包含路径和查询字符串。

  • HTTP/1.1 是协议版本。

后面的 Host 和 Accept 是请求头字段。前者说明目标主机,后者表达客户端可以接受的响应内容类型。头字段之后的空行,用于分隔头部与可能存在的请求体。

请求头提供的是请求相关信息,例如内容类型、Cookie 和缓存条件等。请求体则可以承载表单数据、JSON 或上传文件等内容。上面的示例没有请求体。

1.3.2.2 GET 与 POST 的用途

GET 用于获取资源,常见场景包括打开首页、阅读文章和提交搜索条件。按照 HTTP 的语义设计,GET 不应被用来表达删除文章、修改密码等改变业务状态的操作。

POST 用于把数据提交给目标资源处理,例如提交评论、保存编辑内容或上传文件。

在 Flask 中,通常通过request.form读取;查询字符串通过request.args读取; 上传文件通常通过request.files读取。不同位置的数据,应使用相应的接口处理。

还需要注意,POST 请求同样可以带查询字符串。请求方法与参数位置不是简单的一一对应关系,不能把所有 POST 参数都认定为请求体,也不能把所有 GET 信息都只理解为查询参数。

1.3.2.3 响应由哪些部分组成

服务器处理完请求后,会返回响应。下面是一个简化的成功响应:

HTTP/1.1 200 OK

Content-Type: text/html; charset=utf-8

<h1>Article list</h1>

第一行包含协议版本、状态码和状态说明。200表示请求成功。 Content-Type是响应头字段,说明正文是使用 UTF-8 编码的 HTML。 空行之后的内容是响应体,浏览器会根据内容类型处理它。

响应体不一定是 HTML,也可以是 JSON、图片、CSS 或其他数据。即使请求成功,返回的也不一定是一张完整网页。

1.3.2.4 通过状态码理解处理结果

常见状态码可以按以下方式理解:

200

请求成功,具体结果需要结合响应正文判断。

201

请求成功,并创建了新资源。

302、303

常用于重定向,目标地址通常由 Location 响应头给出。

400

请求存在格式或参数等问题,服务器无法按要求处理。

401

请求缺少有效的身份验证凭据。

403

服务器理解请求,但拒绝执行。

404

没有找到对应资源,也可能用于避免透露某个资源是否存在。

409

请求与资源当前状态发生冲突。

422

请求内容能够被理解,但其中的指令或数据不符合处理要求。

500

服务器处理请求时发生内部错误。

具体业务采用什么状态码,需要以接口实现为准。例如,网页应用访问受限页面时,也可能先重定向到登录页,而不是直接返回 401。

1.3.2.5 保存成功与页面跳转是两个步骤

提交表单后,服务器可以先完成保存,再返回重定向响应,让浏览器进入列表页或详情页。这种过程可以概括为:

POST 提交 →\rightarrow 服务器校验并保存 →\rightarrow 返回重定向 →\rightarrow 浏览器发出新的 GET 请求

其中,服务器保存数据和浏览器取得跳转后的页面,是不同的处理步骤。即使保存已经完成,后续页面加载仍可能失败;页面上出现提示,也应结合实际响应与保存结果判断,不能只凭界面现象推断全部过程。

1.4 一次博客访问的网络路线

前面分别介绍了浏览器与服务器、域名与端口,以及 HTTP 请求与响应。 本节把这些概念连接起来,分析一次博客访问从哪里出发、经过哪些程序,以及最终怎样返回页面。

1.4.1 本地回环访问与公网访问

1.4.1.1 本地回环访问

本地开发时,可以在自己的电脑上启动 Flask,再使用同一台电脑上的浏览器访问。例如:

http://127.0.0.1:8000/

其中, 127.0.0.1 是 IPv4 回环地址,指向发起连接的设备自身; 8000 是服务监听的端口; / 是请求路径。

假设 Flask 正在这个地址和端口上监听,那么访问过程可以概括为:

本机浏览器 →\rightarrow 本机回环地址的 8000 端口 →\rightarrow Flask 应用

浏览器与 Flask 虽然运行在同一台电脑上,但仍然通过网络接口交换 HTTP 请求和响应。 浏览器不会因此直接调用 Python 函数,也不会直接读取 SQLite 数据库。

1.4.1.2 局域网访问与公网访问

如果希望同一局域网中的其他设备访问本机服务,需要使用运行服务的电脑在该网络中的地址。 例如,某台电脑的局域网地址可能是192.168.1.20, 其他设备便可能通过这个地址连接它。能否访问,还取决于程序的监听地址、设备之间的网络连通性以及防火墙设置。

局域网地址通常不能直接作为互联网上的访问入口。 公网访问则需要一个能够从外部网络到达的入口,域名可以解析到这个入口对应的地址。

因此,可以先把几种地址的用途区分如下:

回环地址

用于访问当前设备自身,例如 127.0.0.1。

局域网地址

用于特定局域网范围内的通信,例如常见的 192.168.x.x 地址。

公网入口

为外部访问者提供可到达的服务入口,可能位于服务器本身,也可能由代理或其他网络设施提供。

1.4.2 HTTP、HTTPS 与反向代理的位置

1.4.2.1 HTTP 与 HTTPS 的关系

HTTP 规定浏览器与服务器如何表达请求和响应,HTTPS 则通过 TLS 为这些通信提供传输保护。 浏览器使用 HTTPS 访问网站时,通常会先进行 TLS 握手,并验证服务器提供的证书是否可信、是否与访问的域名匹配。

验证通过并建立受保护的连接后,浏览器再通过该连接交换 HTTP 消息。 应用层仍然具有请求方法、路径、头字段、状态码和响应正文等内容,因此 HTTPS 并没有改变 Flask 处理请求的基本方式。

TLS 主要提供传输过程中的加密、完整性保护和服务器身份验证。 它不能代替应用中的登录、权限检查和输入校验,也不能仅凭连接使用 HTTPS,就判断网站内容本身一定可靠。

1.4.2.2 反向代理位于哪里

实际部署时,浏览器不一定直接连接 Python 应用。 网站可以先由 Nginx 接收外部请求,再把需要应用处理的请求转发给后面的服务。此时,Nginx 就承担反向代理的角色。

从浏览器的角度看,它访问的是网站的公开入口;从 Python 应用的角度看,请求则可能来自本机上的 Nginx,而不是直接来自读者设备。

本项目的部署结构可以按职责概括为:

浏览器 →\rightarrow Nginx →\rightarrow Gunicorn →\rightarrow Flask 应用

浏览器与 Nginx、Nginx 与 Gunicorn 之间可以通过网络连接通信,而 Gunicorn 调用 Flask 通常发生在工作进程内部。

1.4.2.3 一次动态页面请求经过哪些步骤

以使用上述结构访问博客首页为例,可以把过程拆分如下:

  1. 浏览器根据域名取得可用地址,并连接网站的 HTTPS 入口。

  2. Nginx 接收连接,与浏览器完成 TLS 握手,随后取得 HTTP 请求。

  3. Nginx 根据配置判断如何处理请求。对于需要动态生成的首页,把请求转发给 Gunicorn。

  4. Gunicorn 按照 WSGI 接口调用 Flask 应用。

  5. Flask 根据请求方法和路径匹配视图函数,执行数据库查询、业务处理和模板渲染。

  6. Flask 生成响应,由 Gunicorn 和 Nginx 继续传回浏览器。

  7. 浏览器解析返回的 HTML,并按需获取样式、脚本和图片,完成页面显示。

1.4.2.4 静态资源可以走不同的路线

并非所有请求都必须进入 Flask。根据部署配置,Nginx 可以直接提供 CSS、JavaScript 和图片等文件。此时,一条静态资源请求的路线可能是:

浏览器 →\rightarrow Nginx →\rightarrow 静态文件

而首页等动态请求仍然转发给应用。因此,同一次页面加载中的不同请求,可能经过不同的处理路线。

这也解释了为什么网站可能出现“图片正常,但文章页面报错”,或者“文章内容正常,但样式丢失”等现象:它们对应的请求可能由不同环节处理,需要分别检查。

1.4.2.5 按访问路线定位问题

理解完整路线后,排查问题就可以从外到内逐层进行:

  • 域名无法得到预期地址时,先检查域名解析。

  • 无法连接入口时,检查网络、监听端口和入口服务。

  • 证书验证失败时,检查 HTTPS 配置。

  • Nginx 返回 502 时,检查上游服务是否可连接、是否返回了有效响应。

  • 请求已进入 Flask,但页面处理失败时,再检查路由、程序日志、数据库查询和模板。

2 读懂一张网页的结构样式与脚本

2.1 HTML 结构与可交互元素

浏览器取得 HTML 后,需要根据其中的标签、属性和嵌套关系建立页面结构。 对于博客,文章标题、正文、导航、目录和评论区,都需要先通过 HTML 表达出来,再由 CSS 控制外观,由 JavaScript 补充交互行为。

阅读页面代码时,应先看清“有哪些元素、元素之间是什么关系”,再研究它们的样式和行为。

2.1.1 文档树、标签属性与嵌套

2.1.1.1 标签表达内容的结构与含义

HTML 使用标签描述页面内容。例如,标题可以使用h1至h6,段落可以使用p,链接使用a,图片使用img。

下面是一段简化的文章结构:

<article class="post">
    <h1>我的第一篇博客</h1>
    <p>这里是文章的第一段内容。</p>
    <a href="/articles">返回文章列表</a>
</article>

其中, article 表示一块可以独立理解的文章内容; h1 表示标题; p 表示段落; a 表示超链接。

标签不仅影响默认显示方式,也表达内容含义。例如,标题应根据文档层级选择,而不是仅因为某个标题标签的默认字号合适就使用它。 具体字号可以交给 CSS 调整。

2.1.1.2 属性补充元素的信息

HTML 元素可以通过属性提供额外信息。上面示例中的 class="post" 和 href="/articles" 都是属性。

常见属性包括:

id

标识文档中的特定元素,通常应在同一文档中保持唯一,可用于片段定位、CSS 选择和脚本查找。

class

为元素分组,方便多个元素共用样式或脚本行为。一个元素可以具有多个类。

href

指定链接的目标地址。

src

指定图片、外部脚本等资源的地址。

alt

为图片提供替代文本,帮助无法看到图片的用户理解其内容。

2.1.1.3 嵌套关系形成文档树

在前面的文章示例中,article包含标题、段落和链接。这种包含关系形成了树状结构:

可以把article称为这些元素的父元素,把其中的h1、p和a称为子元素。 三个子元素拥有同一个父元素,因此互为兄弟元素。

浏览器解析 HTML 后,会建立 DOM,即文档对象模型。DOM 将页面表示为可以被程序访问的节点树,除了元素节点,还包括文本等其他类型的节点。

后续 CSS 的后代选择器、JavaScript 的元素查找和事件处理,都需要建立在这层结构关系之上。

2.1.2 超链接、表单、输入框和按钮

2.1.2.1 超链接用于访问其他位置

超链接使用a标签,通过href指定目标。例如:

<a href="/articles">查看全部文章</a>

用户点击后,浏览器通常会导航到目标地址。链接主要表达“前往某处”,按钮主要表达“执行操作”。

2.1.2.2 区分 id 与 name

id="post-title"与name="title"看起来都在给输入框命名,但用途不同。

id主要用于在页面中定位元素,例如让标签、CSS 或 JavaScript 找到这个输入框;

name则通常决定表单提交时使用的字段名称。

假设用户输入 Flask,浏览器提交的数据中就可以包含:

title=Flask

Flask 后端可以使用下面的方式读取该表单字段:

title = request.form.get("title", "")

这里查找的是name对应的字段名title,而不是输入框的id。 如果只设置了 id,却没有相应的 name,普通表单提交时通常不会产生预期字段。

2.1.2.3 按钮类型决定默认行为

位于表单中的普通button元素,如果没有明确指定类型,默认通常具有提交行为。 因此,编写按钮时应明确表达用途:

  • type="submit" 用于提交表单。

  • type="button" 不会自行提交表单,适合交给 JavaScript 执行其他操作。

  • type="reset" 用于把表单控件恢复到初始值,而不是一律清空。

2.2 CSS 怎样选择与排列元素

HTML 描述页面内容及其结构,CSS 则控制这些内容如何显示。阅读 CSS 时,需要依次回答三个问题:规则选中了哪些元素,同一元素最终采用哪些规则,以及这些规则怎样影响尺寸、位置和颜色。

2.2.1 选择器、层叠、继承与盒模型

2.2.1.1 选择器确定规则作用于谁

CSS 规则通常由选择器和声明块组成。例如:

.post-title {
    color: #333;
    font-size: 24px;
}

其中,.post-title是选择器,表示选择具有post-title类的元素。大括号内的声明分别设置文字颜色和字号。

后代与直接子元素需要区分。 例如,段落如果还被一层div包裹,它仍然可能被.post p选中,但不会被.post > p选中。

2.2.1.2 层叠决定冲突时采用哪个值

同一个元素可能同时匹配多条 CSS 规则。例如:

p {
    color: black;
}

.post p {
    color: blue;
}

文章内部的段落同时匹配这两条规则。在来源、层叠层及重要性等条件相同的情况下,.post p具有更高的选择器优先级,因此颜色为蓝色。

可以先记住:在相同层叠条件下,id 选择器通常比类选择器优先,类选择器通常比标签选择器优先。

2.2.1.3 继承让部分属性传给后代

某些属性可以从父元素继承,例如文字颜色和字体。例如:

.post {
    color: #333;
    font-family: sans-serif;
}

如果后代元素没有其他适用的声明,它们通常可以继承这些文字属性。

但并不是所有属性都会自动继承。宽度、边框和内边距等属性通常不会因为父元素设置了,就自动传给每个子元素。

2.2.1.4 盒模型决定元素占据多少空间

可以把普通元素的布局区域理解为一个盒子,从内到外包括:

  1. 内容区,即文字、图片等实际内容所在的区域。

  2. 内边距,即 padding, 位于内容与边框之间。

  3. 边框,即 border。

  4. 外边距,即 margin, 用于影响盒子与周围元素之间的间距。

下面是一张卡片的尺寸示例:

.post-card {
    box-sizing: content-box;
    width: 300px;
    padding: 20px;
    border: 1px solid #ddd;
}

项目讨论

遇到问题,或者有改进建议?一起聊聊。

评论 4

全网最尊重长发男的账号长发男

哥哥又更新啦,爱你呀,求加好友qaq

3 楼
orange博主
回复 @全网最尊重长发男的账号

qwq

雨云松云神

橘神

2 楼
RedamancyEACS

👍👍👍

1 楼
目录
如何构建一个博客
项目讨论 · 4