news 2026/10/31 11:09:35

《软件体系结构与设计实用教程》笔记(1)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《软件体系结构与设计实用教程》笔记(1)

软件工程方法学用处?

面向对象方法

面向对象方法是分析对象本身,在UML中有常用的表达方式,如用例图和类图。

类图

在教科书上,通常会介绍类图是由类名,属性·,方法和性质层组成的,太晦涩难懂了,我们直接从Java代码的角度来讲解这个概念。

public class User { private String nickname; // 昵称 private int level; // 等级 protected int points; // 积分 public String getNickname() { return nickname; } public void publish(Content c) { // 发布作品 } private void recalcLevel() { // 内部重算等级,外部不可见 } }

比如有一个User类,类名也就是User,用户有昵称,等级和积分这么一些属性,方法名就是这个类能够干什么,那所谓的性质层就是对应访问修饰符,比如public是公有的,在类图中用➕表示,其他的见下面这个表格自行查阅对照。

符号

读作

对应 Java 关键字

+

公有

public

-

私有

private

#

保护

protected

~

包内

(默认/包级私有)

代码对应的类图如下图所示

public class User { // ┌──────────────────┐ // │ User │ ← 类名 private String nickname; // │──────────────────│ private int level; // │ - nickname:String│ ← 属性名 protected int points; // │ - level:int │ (性质层 = 前面的符号) // │ # points:int │ public String getNickname() { ... } // │──────────────────│ public void publish(Content c) { ... } // │ + getNickname():String│ ← 方法名 private void recalcLevel() { ... } // │ + publish(c:Content) │ } // │ - recalcLevel() │ // └──────────────────┘

类图中的关系

一般化关系

abstract class Content { // 爹:一般的 protected int likeCount; public void like() { likeCount++; } } class PixelPainting extends Content { // 儿子:特殊的 // 自动拥有 likeCount 和 like() }

像泛化/继承就是一种一般化的关系,比如上述示例代码,“儿子”不需要重写“爸爸”的代码就能用它的代码。当然一般化关系也不止类与类之间的继承关系,接口与接口之间的继承关系也是可以的!如下面这个示例代码

interface Exportable { void exportPng(); } interface Printable extends Exportable { // ← 接口继承接口,也是泛化! void print(); // print() 之外,还自动拥有了 exportPng() 这个承诺 }

一个接口可以继承多个接口,如下面示例代码

interface Multi extends Exportable, Shareable { } // 接口可以继承多个接口

当然不止继承,接口实现也算是一种一般化关系,用书上的原话就是Java语言中一般化关系就是可以直接翻译成关键字extends和implements。

关联关系

class A { private B b; // ← 有这一行,就是关联。就这么简单 }

关联是两个类之间的连接,可以是双向的也可以是单向的。

单项关联
class User { private String nickname; } class Content { private User author; // ← 关联:Content 长期持有 User }

对应的图如下所示:

User ←──────── Content 实线箭头(可省略箭头只画实线) 1 *(一个作者可发多个作品)
双向关联
class User { private List<Content> published; // ← 我发布的作品 } class Content { private User author; // ← 这作品的作者 }
自关联
class User { private List<User> following; // 我关注的人(还是 User 类型!) private List<User> followers; // 关注我的人 }

关联是用直线箭头表示的,这一点要记住!

依赖关系

// 关联:存成成员变量 —— 长期认识 class User { private Wallet wallet; // 钱包天天在,随时用 public void pay() { wallet.deduct(); } } // 依赖:只写在方法参数里 —— 临时借用 class User { public void publish(TagValidator v) { // 用完就还,不保存 v.check(this); } }

关联是可以随时用的,而依赖是用完就还的那种

上面的截图是我自己的项目的部分代码截图,可以发现这种就是一种关联关系,用实线箭头表示,

而这则是一种依赖关系,用虚线箭头表示。

聚合关系

class Car { private String plate; // 车自己独立存在 public Car(String plate) { this.plate = plate; } } class Fleet { // 车队(整体) private List<Car> cars; // ★ 关键:Car 是外部 new 好"传进来"的 public Fleet(List&lt;Car&gt; cars) { this.cars = cars; } public void addCar(Car c) { cars.add(c); } } // 使用方: Car myCar = new Car("京A88888"); // 车在车队外就被创建了 Fleet fleet = new Fleet(List.of(myCar)); // 就算 fleet = null(车队解散),myCar 还好好的,还能加别的车队

聚合关系讲的是一个类中有哪些可以用的成分组成,这个还得结合语义看看,虽然从Java语法角度来讲和关联关系无区别,但是可以从用法的角度来区分二者。

类图如下所示:

┌─────────┐ ┌─────────┐ │ Fleet │◇───────────│ Car │ └─────────┘ 1 * └─────────┘ ↑ 空心菱形 ◇,画在【整体】这一端

菱形在整体这一侧

组合关系

组合关系相当于人和心脏之间的关系

class Heart { private int bpm; public void beat() { /* ... */ } } class Person { // 人(整体) // ★ 关键:Heart 是在自己内部 new 出来的 private Heart heart = new Heart(); public void exercise() { heart.beat(); } } // 外界拿不到这个 heart: // new Person().heart → 编译不过(除非提供 getter,那也只是读) // person 没了 → heart 也没人引用了 → 被垃圾回收,一起消失
┌─────────┐ ┌─────────┐ │ Person │◆───────────│ Heart │ └─────────┘ 1 1 └─────────┘ ↑ 实心菱形 ◆,同样贴着【整体】这一端

总结一下,一般化关系就是我们平时用的多的泛化与实现,关联关系最常见的例子就是平时自动注入一个类,依赖关系就是在传参数的时候用上这个类,聚合关系和组合关系得结合语境看看。

软件体系结构

组成是什么书上例子你项目例子
组件有独立功能、可重用的模块Spot 类(存坐标)登录模块、聊天模块
连接件组件之间怎么交互Trans.move() 方法调用HTTP、WebSocket、JDBC
实例把上面俩串起来演示Test 类调 move() 改坐标点赞→发消息→通知作者

简单来说组件是可以复用的软件结构单元,小到变量,函数,大到类,文件,程序,数据库,都是组件。

如果还是觉得很抽象,咱们就来看看Java示例代码吧。

// 组件 1:Spot(存坐标的零件) class Spot { int x, y; int getX() { return x; } int getY() { return y; } } // 组件 2:Trans(负责移动的零件) class Trans { // 连接件:通过方法调用使用 Spot void move(Spot s, int dx, int dy) { s.setX(s.getX() + dx); s.setY(s.getY() + dy); } } // 实例演示:Test 把两个组件连起来跑一遍 class Test { public static void main(String[] args) { Spot s = new Spot(); // 造组件 1 Trans t = new Trans(); // 造组件 2 t.move(s, 3, 5); // 连接件生效:坐标变了 System.out.println(s.getX()); // 输出 3 } }

上述代码中,move是连接件,然后Test类中的main方法其实就是一种实例。

软件体系结构研究的内容

软件体系结构的研究内容 = 围绕"结构图"的 5 个问题: 怎么把图画出来?——体系结构描述(ADL) 人话:图纸得有规范画法。ADL 就是专门画这张图的"专业制图工具" (Acme、Wright、Rapide) 用哪种搭法?——体系结构风格 人话:前人总结的经典套路(管道-过滤器、C/S、分层……) 搭得好不好?——体系结构评估 人话:图纸画完先检查:性能行不行?好改不好改?别等楼盖塌了才发现 能不能照抄?——体系结构复用与演化 人话:好的搭法下个项目接着用;系统运行后要扩建,图纸怎么改 怎么照着盖?——基于体系结构的开发 人话:图纸 → 施工的全流程:分析→定结构→设计→编码,体系结构 贯穿软件生存周期

用大白话来讲就是要研究怎么样才能让别人看得懂我的项目,所以写成一种规范的写法

构件:Vue 前端、SpringBoot 服务、MySQL、Redis
连接件:HTTP(前后端)、JDBC(后端→MySQL)
约束:前端不许直连数据库;每个请求必须带 token

比如上面这种写法

然后就是编写代码的时候采用什么风格,有B/S分层风格,有管道-过滤器风格。

结构风格研究完之后就研究评价方法

时刻你做的事研究内容
开工前把“三块+怎么连+什么规矩”写成条文① 描述
定套路挑了 B/S + 分层 + 局部事件驱动② 风格
动手前自问“加直播要改几处”③ 评估
下个项目骨架直接搬去二手书平台④ 复用
写代码每行代码照图填空

⑤ 开发

软件体系结构风格

和建筑风格一样,软件体系也有它的结构风格,

MVC

// Controller:只管接请求(C) @RestController class ArticleController { @GetMapping("/api/articles") Result list(int page) { return articleService.list(page); } } // Model:管数据和业务(M) @Service class ArticleService { public Result list(int page) { return Result.ok(articleMapper.selectPage(page)); // 读写数据 } } // View:管显示(V)—— 前端 Vue 渲染,后端只返回数据

分层风格

Controller → Service → Mapper,一层只调相邻下一层: class ArticleController { private ArticleService service; // 只认识 Service } class ArticleService { private ArticleMapper mapper; // 只认识 Mapper } class ArticleMapper { /* 只管 SQL */ }

C/S B/S架构

面向对象设计

问题域部分的设计

比如分析阶段可能是分析出这种模型

A B C 想同时继承 A 和 B \ / (比如:在职研究生,既是研究生,又是教师) \ / 但 Java 只许 extends 一个! C

但是一个类不可能同时继承多个类【接口可以继承多个接口】

这种情况怎么解决?

class A { } class B extends A { } // B 先继承 A class C extends B { } // C 再继承 B —— 串成 A→B→C 一条链

Java 不支持多重继承,设计中需将分析模型中的多重继承化为单重继承,方法包括:调整继承结构串成单链、抽取一般类消除重复属性、将其中一个继承关系改为组合(对象成员)等。

控制驱动部分设计

这个说大白话就是设计“发动机”——什么东西负责把流程跑起来,想清楚什么程序先跑什么程序后跑。

举个例子

// 被动类:不调它,它就静静躺着 @Service class ArticleService { public List<Article> list() { ... } // 没人调用 = 永远不执行 } // 主动类:系统一启动,它自己就转起来了 @Component class LikeCountSyncTask { // ← 主动类 @Scheduled(cron = "0 */10 * * * ?") // 每 10 分钟自己醒一次 public void syncRedisToDb() { ... } }

component注解能够在系统启动的时候加载这个类,而service注解下的这个类需要手动调用才能够运行起来。

这两个类区别在于一个是主动类一个是被动类

被动类主动类
谁让它动别人调用它才动自己会主动跑起来
人话像工具:你不按它,它不动像工人:到点自己干活
你的项目ArticleService(被 Controller 调)定时任务、WebSocket 监听线程、消息消费者

经典软件体系结构风格

接下来我将详细介绍主程序-子程序,面向对象风格。

主程序-子程序风格

// 主程序:唯一的入口,控制整个流程 public class Main { public static void main(String[] args) { // ← 一切从这里开始 int a = 10, b = 5; System.out.println(minus(a, b)); // 按顺序喊子程序 System.out.println(print_msg("hello")); System.out.println(printx(999)); } } // 子程序:被喊到才动,是"被动类" static int minus(int a, int b) { return a - b; } static void print_msg(String s) { ... } static int printx(int x) { return x; }

可以看到上面这个代码,连接件是调用-返回机制。

这种风格的核心思路就是分而治之:把一个大而复杂的程序系统,拆成主程序加一堆子程序,主程序负责调度,子程序负责干活。好处很直接——复杂任务被分解成许多容易处理的小任务,谁写哪个子程序分得清清楚楚,开发分工容易,出了问题也容易定位,维护起来相对省心。

但它的缺点同样明显,可以归纳成下面几条:

  • 程序规模一大,开发就变慢:子程序越拆越多,主程序里的调用顺序越来越长,改一处要顺着调用链一路看下去。
  • 测试困难:子程序之间靠参数和全局数据耦合,单独测一个子程序往往要先把它的上下游都准备好。
  • 重用性差:子程序通常是为某个具体流程写的,换个场景就很难直接搬走。
  • 数据安全差:大量使用全局变量和共享数据,谁都能改,一处改错全盘受影响。
  • 难以开发大型软件和图形界面应用:界面需要事件驱动、随时响应,而这种风格是顺序调用、一条道走到黑,交互性天然吃亏。
  • 数据结构一变就难维护:数据结构往往被多个子程序共享,改一个字段可能要连带改一大片子程序的参数和逻辑。

一句话理解:主程序-子程序风格适合流程固定、以计算和数据处理为主的中小型程序;一旦系统变大、需要频繁交互或数据结构经常变动,它的短板就会集中暴露,这也是后来面向对象风格、事件驱动风格等被提出的原因。

总结优点缺点,主程序子程序优点是化繁为简,任务分解;分工明确,便于开发,但是缺点也有,如果数据结构一变就很难维护,程序太长运行起来也消耗更长的时间资源,并且子程序重用性差,难以开发大型的软件和图形化界面。

面向的对象风格

/* ========== 1. 作者:自己的积分和粉丝自己管 ========== */ public class Author { private final String name; private int points; private List<String> fans = new ArrayList<>(); public Author(String name) { this.name = name; } // 行为住在对象里:加积分是"作者自己"的事 public void addPoints(int delta) { this.points += delta; } public void notifyFans(String message) { for (String fan : fans) { System.out.println("→ 通知粉丝 " + fan + ":" + message); } } public String getName() { return name; } public int getPoints() { return points; } public void addFan(String fan) { fans.add(fan); } }

面向对象风格具有抽象,封装,继承和多态等特点。

// 这是一个抽象接口,定义了“发送消息”这个能力,但不管具体怎么发 public interface MessageSender { boolean sendMessage(String receiver, String content); } // 客户端代码只依赖抽象接口 public class NotificationService { // 依赖抽象,而不是具体的实现类 public void notifyUser(MessageSender sender, String user, String msg) { sender.sendMessage(user, msg); // 我不关心底层是发邮件还是发短信 } }
public class RoomGroup { private Long groupId; // 私有属性,外界不能直接修改 private String groupName; // 私有属性 private int memberCount; // 私有属性 // 对外提供安全的访问接口 public String getGroupName() { return groupName; } public void setGroupName(String groupName) { // 封装了业务逻辑:名字不能为空,且长度有限制 if (groupName == null || groupName.trim().isEmpty() || groupName.length() > 50) { throw new IllegalArgumentException("群名称不合法"); } this.groupName = groupName; } // 成员数量只能增加或减少,不能随意修改数值 public void addMember() { this.memberCount++; } }

抽象

// 三种作品:文章、像素画、游戏 Demo // 共同点:都有 id、作者、赞数,都要发布 // 不同点:怎么校验、怎么存、怎么通知——各不一样 abstract class Content { protected Long id; protected Author author; protected int likeCount; public abstract void validate(); // 只声明"要有校验",不说怎么校验 public abstract void store(); // 只声明"要能存",不说存哪 }

这是体现出代码的抽象写法

封装

abstract class Content { private int likeCount; // 私有:外部不能直接改 public void like() { // 只开放这个入口 likeCount++; // 怎么加,规则由我定 } public int getLikeCount() { // 只读,不能写 return likeCount; } } // 外部代码: c.likeCount = 999; // ❌ 编译不过(private) c.like(); // ✅ 唯一合法方式

写Java写的多的朋友肯定对这种写法不陌生吧!

继承

class Article extends Content { private String body; public void validate() { if (body.isEmpty()) throw new BizException("空文章"); } public void store() { articleMapper.insert(this); } // ↑ 只写了这两个,likeCount、like()、author 全是白拿的 } class PixelPainting extends Content { private int[][] grid; public void validate() { if (grid.length > 64) throw new BizException("画布太大"); } public void store() { ossService.upload(grid, "png"); } }

多态

// 调用方:只认识 Content,完全不知道具体类型 public void publishAll(List<Content> works) { for (Content c : works) { c.validate(); // ← 多态在这里发生 c.store(); // ← 多态在这里发生 } } // 用法: List<Content> works = List.of( new Article(...), new PixelPainting(...), new GameDemo(...) ); publishAll(works); // 一行代码,三种行为各走各的

用大白话概括,抽象是定义有哪些概念,封装则是把数据隐藏起来只暴露方法,继承是子类复用父类的东西只写差异,然后多态稍微抽象一点,你可以简单理解成

面向对象的优缺点如下所示

数据流风格

数据流风格分为批处理风格和管道过滤器风格。

批处理风格

public class TranscriptMain { public static void main(String[] args) throws IOException { File infile = new File("transcript.txt"); // 原始数据:一批学生记录 File outfile = new File("transcript_result.txt"); // 结果文件 List&lt;String&gt; lines = readLines(infile); // ① 整批读入 sortLines(lines); // ② 整批排序 writeLines(outfile, lines); // ③ 整批写出 } }

这个是批处理风格,关注的是数据如何流动,而不是关注谁调用谁。

注意List<String> lines—— 它是整个文件全读进内存再处理。这就是批处理的“批”字:数据必须先攒齐,才能开工。

优点

缺点

管道-过滤器风格

书上说这种风格有很多优点,比如组件具有高内聚低耦合有点,可以复用,可以扩展,并且支持并行执行,但是不适合处理交互应用

如果还不懂可以看看下面这个图,管道相当于传送带,过滤器相当于一个一个工人,数据就是传送带上的物品

这个IMServer代码就能够很好的体现出我上面说的这种风格,可以看到管道pipeline,各种handler处理器,数据就是从处理器中通过。

管道过滤器优点有以下几点:首先是代码体现出高内聚低耦合的特点;然后管道过滤器分工明确;再者就是可以将整个系统行为看成多个过滤器的合成,比较简单;可复用性:管道过滤器可以复用,让开发工作方便起来了;可扩展性:就像IM服务器,可以扩展新增自定义的过滤器;可验证性:在管道-过滤器/批处理架构中,因为每个“过滤器(Handler)”或“处理步骤”都是高度独立、接口明确的,所以它的可验证性非常高。;支持并行执行

但是这种风格也有缺点:

隐式调用风格

显式调用是“点名做事”,隐式调用是“拉响警报”——拉警报的人只负责拉,救火的各自冲上来。代价是:你再也看不出来谁会响应,灵活性换来了可读性。

// 发布者:只管广播,不知道谁会收到 public void like(int contentId) { contentMapper.incrLike(contentId); // 干自己的活 eventBus.post(new LikedEvent(contentId)); // 广播:"点赞了!"——完事 } // 订阅者 1:收到事件,自己决定做什么(通知作者) @Subscribe public void onLiked(LikedEvent e) { pushService.notifyAuthor(e.getContentId()); } // 订阅者 2:也收到了,自己决定做什么(加积分) @Subscribe public void onLiked2(LikedEvent e) { authorService.addPoints(e.getContentId()); }

层次风格

表示层(Vue 页面) ↓ 只能往下调 业务层(Service) ↓ 数据层(Mapper/DB)

仓库风格

[组件A] [组件B] \ / [中央数据仓库] ← 所有人围着它转 / \ [组件C] [组件D]
// 组件 A:只负责写数据进仓库,不知道谁会读 @Service public class LikeService { @Autowired private LikeMapper likeMapper; // 仓库(MySQL) public void like(Long contentId, Long userId) { likeMapper.insert(contentId, userId); // 写进中央仓库,完毕 // 它不知道有个"统计模块"会去读这张表 } } // 组件 B:只负责从仓库读,不知道数据谁写的 @Service public class RankService { @Autowired private LikeMapper likeMapper; public List&lt;Content&gt; hotList() { return likeMapper.selectByLikeCountDesc(); // 从同一个仓库读 } }

这段代码也算是仓库风格!

解释器风格

// 规则是数据,不是代码 —— 改规则不用改代码 private static final List<String> WHITE_LIST = List.of( "/api/auth/**", ); public boolean preHandle(HttpServletRequest request, ...) { String uri = request.getRequestURI(); for (String pattern : WHITE_LIST) { if (MATCHER.match(pattern, uri)) { // ← 解释器:现场解释这条规则 return true; } } return false; }

读规则,现场解释这种代码风格叫解释器风格。

反馈控制环风格

// 简化版自动限流:测当前 QPS,超阈值就限流,降下来再放开 @Component public class AdaptiveRateLimiter { private static final int THRESHOLD = 1000; // 目标:QPS 不超过 1000 private volatile boolean limited = false; @Scheduled(fixedRate = 1000) // ← 每秒采一次样(传感器) public void controlLoop() { int qps = Metrics.getCurrentQps(); // ① 测量 if (qps &gt; THRESHOLD) { limited = true; // ② 超了 → 收紧 } else if (qps &lt; THRESHOLD * 0.7) { limited = false; // ③ 降了 → 放开 } // 循环往复,这就是"反馈" } public boolean allowRequest() { return !limited; } }

分布式软件体系结构风格

分布式系统是指基于网络环境,功能和数据分布在网络相连的多个计算机中,通过他们之间通信协作完成的各项功能的系统。

那为什么要有分布式软件体系结构呢?其实以前是集中式,也就是一台机器干所有事,这种风格风险很高,容易出现主机挂了的现象。

分布式体系结构风格

文件共享体系结构

P2P体系结构风格

2P(Peer-to-Peer)体系结构中,没有专用的服务器,每个节点既是客户端又是服务器(对等体),系统由对等节点组成,节点之间直接通信,通常为整个网络提供服务。

三层C/S体系结构风格

B/S体系结构风格

B/S C/S混合软件结构

中间件

总结

集中式(主机+终端) ↓ 加了文件服务器 文件共享(NAS,无并发控制) ↓ 客户端变胖 两层 C/S(胖客户端:快但不安全、难维护) ↓ 中间加一层业务层 三层 C/S(瘦客户端 + 应用服务器 + 数据库) ↓ 客户端换成浏览器 B/S(免安装、零维护) ← 你的 Vue + SpringBoot + MySQL 就是它 (旁支)P2P:没有中心,人人皆服务器 (贯穿始终)中间件:RPC / JDBC / ODBC / 消息队列 —— 屏蔽差异的胶水

MVC风格与Struts框架

什么是MVC

MVC发展史

MVC的三种组件与连接件

MVC在Java技术中的应用

Structs框架

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 11:08:23

月底最后3天,老板别只顾冲业绩——你的账可能正在“裸奔“

有个老板跟我吐槽&#xff1a;每月底累死累活冲业绩&#xff0c;结果年底S局一预警&#xff0c;罚款比利润还高。他不是个例。很多老板把"做账"全甩给财务&#xff0c;自己从不看一眼。可你不知道的是——S局的大数据风控&#xff0c;是按月扫的。你月底不动&#xf…

作者头像 李华
网站建设 2026/10/31 11:07:10

如何使用 work buddy 在电脑上打开美橙互联后台优化官网吗?如何操作?

目录 一、两种工作模式区分 模式A(推荐小白,稳定、安全)【优先用】 模式B:Agent Browser浏览器技能辅助模式(进阶Beta版) 二、小白完整操作指南(模式A + 模式B) 前置准备 步骤1:诊断现有官网 步骤2:产出优化物料(你直接复制粘贴到美橙后台) 步骤3【进阶】Ag…

作者头像 李华
网站建设 2026/10/31 11:01:07

Linux FFmpeg深度解析之av_dump_format调用流程与实战(十一)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》作者 博主新书推荐&#xff1a;《Android系统多媒体进阶实战》&#x1f680; Android Audio工程师专栏地址&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; Android多媒体专栏地址&a…

作者头像 李华
网站建设 2026/10/31 10:58:37

【Python毕业设计】Django+Vue+MySQL基于HTML5的旅游网站的设计与实现|旅游信息管理系统、景点管理与旅游攻略分享平台(功能演示+核心代码)【计算机毕业设计课程设计】

本系统以旅游信息展示和旅行经验分享为应用场景&#xff0c;采用 Python Django Vue MySQL 开发&#xff0c;覆盖前台用户端和后台管理端。用户端已实现用户注册登录、个人资料管理、景点分类与关键词查询、景点详情阅读、旅游攻略浏览与发布、收藏点赞评论、社区发帖与交流…

作者头像 李华
网站建设 2026/10/31 10:57:19

机房综合布线怎么做标签管理(TIA-606 标准 + Excel 台账模板)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华