SwiftUI 如何实现 Infinite Scroll?

作者:RickeyBoy日期:2026/3/27

欢迎点个 star:github.com/RickeyBoy/R…

面试题:用 SwiftUI 实现一个无限滚动列表,支持分页加载。

这道题我在面试中遇到过好几次,说实话第一次答的时候以为随便写个 LazyVStack + onAppear 就完事了。后来才发现,面试官真正想考的不是你会不会用 API,而是你对状态管理、性能优化、Task 生命周期这些东西到底理解多深。

我的思路是从最简方案出发,一步步暴露问题、一步步优化。在开始写代码之前,先聊一下架构选型。

为什么选 MVVM?

先说一下 SwiftUI 里常见的架构选择。MVC 就不聊了,那是 UIKit 时代的标配,Controller 跟 UIKit 强耦合,到了 SwiftUI 里根本没有 UIViewController 这个角色,MVC 自然也就退出舞台了。

SwiftUI 里最常见的架构,从简单到复杂大概是这么几个:

架构特点适合场景
MV(Model-View)没有 ViewModel,状态直接放 View 里,Apple 官方示例的典型写法逻辑简单的页面
MVVM抽出 ViewModel 管理状态和逻辑,SwiftUI 里最主流的选择中等复杂度,需要可测试性
TCA单向数据流,State + Action + Reducer + Effect,强约束大型项目,需要严格的状态管理

其中 MV 是最基础的,逻辑简单的页面,@State 往 View 里一放就完事了,Apple 自己的 WWDC 示例大量都是这么写的。但 infinite scroll 涉及分页状态、加载状态、错误处理、Task 生命周期管理这些东西,全塞 View 里会很乱。抽一个 ViewModel 出来专门管理这些状态,View 只负责渲染和转发用户操作,职责就清晰多了。

所以这道题用 MVVM 是最合适的,不是因为 MVVM 最好,而是这个场景的复杂度刚好适合。并且采用 MVVM 结构规整,可拓展性也强,从面试回答的角度来讲也是正好的。

而 SwiftUI 天然就鼓励这种模式,@Observable 本身就是 binding 机制,ViewModel 状态一变,View 自动更新,不需要手动同步。我们后面的代码就是按这个思路来的。

一、最小可用版本

先写一个能跑的最简版本。

核心思路很简单:LazyVStack 只在 item 即将可见时才实例化 View,我们利用 onAppear 检测"最后一个 item 出现了",然后触发下一页请求。

Model

1struct Item: Identifiable, Equatable {
2    let id: String
3    let title: String
4}
5
6struct PageInfo {
7    let endCursor: String?
8    let hasNextPage: Bool
9}
10

ViewModel

1@MainActor @Observable
2final class ItemListViewModel {
3    private(set) var items: [Item] = []
4    private var pageInfo: PageInfo?
5
6    func loadNextPage() async {
7        let response = try? await APIService.fetchItems(after: pageInfo?.endCursor)
8        guard let response else { return }
9        items.append(contentsOf: response.items)
10        pageInfo = response.pageInfo
11    }
12}
13

View

1struct ItemListView: View {
2    @State private var viewModel = ItemListViewModel()
3
4    var body: some View {
5        ScrollView {
6            LazyVStack {
7                ForEach(viewModel.items) { item in
8                    ItemRow(item: item)
9                        .onAppear {
10                            if item == viewModel.items.last {
11                                Task { await viewModel.loadNextPage() }
12                            }
13                        }
14                }
15            }
16        }
17        .task { await viewModel.loadNextPage() }
18    }
19}
20

代码很短,逻辑也直白:

  1. 每当最后一个 item 出现在屏幕上,就触发 loadNextPage()
  2. loadNextPage() 请求后台去 fetch,拿到数据然后塞进 items 中
  3. View 检测到有更新,自动刷新页面

一句话总结:最后一个 item onAppear 的时候,就进行请求。

1.1 分页方式:cursor vs offset

可能有同学会问:为什么 fetchItems(after: cursor) 用的是 cursor,而不是传统的 pageoffset

分页一般有两种方式:

  • Offset-basedfetchItems(page: 3, size: 20),按页码或偏移量取数据
  • Cursor-basedfetchItems(after: "abc123"),传上一页最后一条的标识,从那里往后取

对于 infinite scroll 这种场景,cursor-based 更合适。详细对比一下:

Cursor-basedOffset-based
数据一致性不受中间插入/删除影响插入新数据会导致重复或遗漏
性能数据库只需定位到 cursor 后续大 offset 需要 skip N 行
适用场景实时 feed、社交流固定数据集、后台管理列表

简单来说,cursor-based 更适合"数据随时在变"的场景(比如社交 feed),offset-based 更适合"数据基本不变"的场景(比如后台管理列表)。infinite scroll 的数据通常是动态的,所以用 cursor-based。

1.2 LazyVStack vs List

可能有同学会问:为什么用 LazyVStack 而不是 List

先说浅显的回答:LazyVStack 布局更自由,没有 List 自带的分割线、背景色、cell 样式这些限制,适合高度自定义的 UI。而 List 开箱即用,自带滑动删除、拖拽排序这些交互,适合标准列表场景。

当然,如果想要深入回答,还有可以继续。二者还有一个关键区别其实是内存模型

LazyVStackList
View 回收❌ 不回收,创建后常驻内存✅ 内部回收机制
内存增长随滚动距离线性增长基本恒定
自定义布局完全自由受限于 List 样式
万级数据可能有内存压力表现更好

为什么会有这个区别?因为它们底层的实现不一样。List 底层是基于 UICollectionView(iOS 16 之前是 UITableView),天然有 cell 回收复用机制,滚出屏幕的 cell 会被回收,滚入时再复用,所以内存占用基本恒定。而 LazyVStack 底层只是一个普通的布局容器,"Lazy" 的意思是延迟创建,item 滚入可见区域时才创建 View,但创建之后就一直留在内存里,不会回收。

所以如果列表数据量很大(比如社交 feed 那种上万条的),List 在内存上更有优势。如果需要高度自定义的 UI,那就用 LazyVStack,但要心里有数:用户滚得越远,内存占用越大。

1.3 为什么加 @MainActor

上面的 ViewModel 代码加了 @MainActor,这个很容易被忽略但其实很关键。

@Observable 本身不会自动保证在主线程更新状态。而我们的 loadNextPage() 是在 Task 里通过 await 拿数据,await 之后的代码在哪个线程执行是不确定的。如果恰好在后台线程执行了 items.append(...),SwiftUI 收到状态变更通知后会在后台线程刷新 UI,这就会导致紫色警告("Publishing changes from background threads is not allowed")甚至崩溃。

加上 @MainActor 之后,这个类的所有属性访问和方法调用都会被隔离到主线程,从根源上避免线程安全问题。

另外补充一下:Swift 6.2(Xcode 26)引入了模块级别的 Default Actor Isolation 设置,可以把整个模块的默认隔离改为 MainActor,开启之后所有类型都默认跑在主线程,不用再手动加 @MainActor。但这是一个 opt-in 的设置,默认值还是 nonisolated,而且不是所有项目都会立刻升级。所以目前来说,显式写 @MainActor 仍然是更稳妥的做法。

1.4 几个小细节

有几个代码细节,不影响功能,但代码质量会好不少,属于面试加分项。

private(set) 控制可见性

itemsprivate(set) 修饰,外部只能读不能写。这样 View 就没法直接改 items,所有数据变更都必须经过 ViewModel 的方法,数据流向是单向的。这个习惯在 MVVM 里很重要,不然 View 和 ViewModel 的职责边界很容易模糊。

让 Item 遵循 Equatable

上面的代码里 Item 已经加了 Equatable,所以判断"是不是最后一个"可以直接写 item == viewModel.items.last,不用绕一圈去比 id。后面加叠加更多功能的时候也可以用,代码更简洁。

.task = .onAppear + Task

View 里首次加载用的是 .task { await viewModel.loadNextPage() },这其实等价于在 .onAppear 里手动创建一个 Task。但 .task 有个好处:当 View 消失时会自动 cancel 这个 Task。手动写 Task {} 的话你得自己管 cancel,容易漏掉,所以首次加载优先用 .task

二、防重复请求

上一个最基础的版本,有个明显的问题:快速滚动时 onAppear 有可能会被多次触发,导致同一页被重复请求了。

怎么解决?思路也很直接:加一个 isLoading 标记 + hasNextPage 判断,双重 guard,然后通过这些属性来判断是否需要发送请求。

1@MainActor @Observable
2final class ItemListViewModel {
3    private(set) var items: [Item] = []
4    private(set) var isLoading = false
5    private var pageInfo: PageInfo?
6
7    var canLoadMore: Bool {
8        guard let pageInfo else { return items.isEmpty } // 首次加载
9        return pageInfo.hasNextPage && !isLoading
10    }
11
12    func loadNextPage() async {
13        guard canLoadMore else { return }
14        isLoading = true
15        defer { isLoading = false }
16
17        let response = try? await APIService.fetchItems(after: pageInfo?.endCursor)
18        guard let response else { return }
19        items.append(contentsOf: response.items)
20        pageInfo = response.pageInfo
21    }
22}
23

canLoadMore 这个 computed property 干了两件事:

  • 没有下一页时不请求(通过后端返回的 pageInfo.hasNextPage 来判断)
  • 正在加载时不重复请求(通过 isLoading 来判断)

2.1 小细节

defer 管理状态翻转

注意 isLoading 的写法:开头设为 true,然后紧接着 defer { isLoading = false }。这样不管后面是正常返回还是提前 returnisLoading 都会被重置回 false

如果不用 defer,你就得在每个 return 之前手动加一句 isLoading = false,路径一多很容易漏掉,漏掉的后果就是列表永远卡在 loading 状态,再也加载不了下一页。

canLoadMore 作为 computed property

把"能不能加载"的判断收到一个 computed property 里,而不是在 loadNextPage() 里写一堆 if。好处是逻辑集中,后面要加新条件(比如错误状态下不加载)直接改这一个地方就行,调用方不用动。

三、提前预加载:Threshold Prefetch

目前的逻辑是"最后一个 item 出现了才开始加载",那么用户的感受就是:滚到底 → 停顿 → 等数据 → 新数据出现。那个停顿虽然可能只有几百毫秒,但体感上还是挺明显的。

怎么办?提前触发。 不等最后一个 item,而是在还剩 N 个 item 时就开始加载下一页。

1// View
2ForEach(viewModel.items) { item in
3    ItemRow(item: item)
4        .onAppear { viewModel.onItemAppear(item) } // View 层仅透传,将逻辑交给 ViewModel
5}
6
7// ViewModel,新增 prefetch threshold
8private let prefetchThreshold = 5
9
10func onItemAppear(_ item: Item) {
11    guard let index = items.firstIndex(of: item),
12          index >= items.count - prefetchThreshold else { return } // 判断是否该加载下一页了
13    Task { await loadNextPage() }
14}
15

这样一来,用户还剩 5 个 item 可以滚的时候,网络请求就已经在跑了,等滚到底部时,数据大概率已经回来了,体验上就是"无缝衔接"。

那 threshold 到底设多少合适?这个纯属经验值,根据具体的数据量、UI 复杂度都相关,5 只是一个经验值。总的来讲就是一个 trade-off:

  • threshold 太小:快速滚动还是会看到停顿
  • threshold 太大:用户可能只看前几条就走了,白白浪费请求

四、Task 取消 + 错误处理

到这里基本功能已经没问题了。接下来聊聊 Task 生命周期管理和错误恢复,这部分在面试里属于加分项。

4.1 Task 取消

为什么需要管理 Task 取消?我们目前的例子中,单一列表的情况可能不需要考虑。但是如果是搜索页面的列表,或者叠加筛选功能,问题就复杂了。

举个具体的例子:

  1. 用户在搜索页搜"咖啡",然后在列表页向下滑动,触发了一个 loadNextPage 的请求 A
  2. 还没等数据回来,用户改成搜"奶茶",请求 B 又发出去了
  3. 这时候网络上同时有两个请求在跑。如果请求 B 先于 A 回来,那么等请求 A 回来的时候,用户就会发现明明搜索的是“奶茶”,但是却又展示了不少“咖啡”内容。

这种 bug 不是每次都能复现(取决于网络时序),但一旦出现用户会很困惑,所以解决方式就是:发新请求前先 cancel 旧的,被 cancel 的任务即便返回了 response 也不处理

1@MainActor @Observable
2final class ItemListViewModel {
3    private(set) var items: [Item] = []
4    private(set) var isLoading = false
5    private(set) var error: Error?
6    private var pageInfo: PageInfo?
7    private var loadTask: Task<Void, Never>? // 💾 持有当前请求的引用
8
9    func loadNextPage() {
10        guard canLoadMore else { return }
11        loadTask?.cancel() //  发新请求前,先 cancel 旧的
12        isLoading = true
13
14        loadTask = Task { [weak self] in // 🔒 weak self 防止循环引用
15            guard let self else { return }
16            defer { self.isLoading = false }
17
18            do {
19                let response = try await APIService.fetchItems(after: pageInfo?.endCursor)
20                guard !Task.isCancelled else { return } // 🛡️  cancel 了就不写入
21                self.items.append(contentsOf: response.items)
22                self.pageInfo = response.pageInfo
23                self.error = nil
24            } catch {
25                guard !Task.isCancelled else { return } // 🛡️ 同上
26                self.error = error
27            }
28        }
29    }
30
31    func reset() {
32        loadTask?.cancel() //   cancel,再清空
33        items = []
34        pageInfo = nil
35        isLoading = false
36        error = nil
37    }
38
39    // ...
40}
41

4.2 错误重试

错误处理其实是一个很容易被忽略,同时也非常复杂的事情。这里我们的方案是当出现错误的时候,展现一个重试按钮。从 UI 的角度来讲不好看,但实际上面试阶段时间有限,能够展示出有错误处理的思维就可以了。

1@MainActor @Observable
2final class ItemListViewModel {
3    // ...
4    private(set) var error: Error?
5
6    func retry() {
7        error = nil
8        loadNextPage()
9    }
10}
11
1// View  列表底部
2if viewModel.error != nil {
3    RetryButton { viewModel.retry() }
4} else if viewModel.isLoading {
5    ProgressView()
6}
7

4.3 空状态处理

还有一个容易忽略的边界情况:首次加载完成后,后端返回了 0 条数据。

当前的代码里,items 为空有两种可能:一种是"还在加载第一页",另一种是"加载完了但确实没数据"。如果不区分这两种状态,用户看到的就是一片空白,不知道是在等数据还是真的没有内容。

处理方式也很简单,加一个 computed property 判断一下:

1var isEmpty: Bool {
2    !isLoading && items.isEmpty && error == nil && pageInfo != nil
3}
4

这里的关键是 pageInfo != nil,说明至少请求过一次了(首次加载前 pageInfonil)。四个条件同时满足,才说明"确实没数据"。

View 里对应的处理:

1if viewModel.isEmpty {
2    ContentUnavailableView("暂无数据", systemImage: "tray")
3} else if viewModel.isLoading && viewModel.items.isEmpty {
4    ProgressView() // 首次加载中
5} else {
6    // 正常的列表内容
7}
8

这样用户就能清楚地区分"加载中"和"没有数据"这两种状态了。

4.4 用 enum 收敛 View 状态

到这里你会发现,View 层需要处理的状态越来越多:首次加载中、有数据、空数据、出错。如果全用 if/else if 判断,条件一多很容易写乱,漏掉某个分支也不会有编译器提醒。

可以定义一个 enum 来收敛这些状态:

1enum ViewState {
2    case initialLoading    // 首次加载中
3    case loaded            // 有数据,正常展示列表
4    case empty             // 加载完了但没数据
5    case error(String)     // 出错了
6}
7

然后在 ViewModel 里加一个 computed property,从现有属性推导出当前的 View 状态:

1var viewState: ViewState {
2    if let error, items.isEmpty {
3        return .error(error.localizedDescription)
4    }
5    if isLoading && items.isEmpty {
6        return .initialLoading
7    }
8    if isEmpty {
9        return .empty
10    }
11    return .loaded
12}
13

注意这里的关键:ViewState 是 computed property,不是存储属性。底层的数据源还是 isLoadingitemserrorpageInfo 这些独立属性,viewState 只是把它们组合成 View 更容易消费的形式。这样既不会出现之前 LoadingState enum 耦合状态的问题,又让 View 的代码变得很干净:

1var body: some View {
2    Group {
3        switch viewModel.viewState {
4        case .initialLoading:
5            ProgressView()
6        case .empty:
7            ContentUnavailableView("暂无数据", systemImage: "tray")
8        case .error(let message):
9            ErrorView(message: message) { viewModel.retry() }
10        case .loaded:
11            ScrollView {
12                LazyVStack(spacing: 0) {
13                    ForEach(viewModel.items) { item in
14                        ItemRow(item: item)
15                            .onAppear { viewModel.onItemAppear(item) }
16                    }
17                    loadingFooter
18                }
19            }
20        }
21    }
22    .task { viewModel.loadNextPage() }
23}
24

switch 替代 if/else if,每个分支对应一种状态,漏掉任何一个编译器都会报错。用 Group 包裹 switch 是为了能在外层挂 .task 触发首次加载。

五、完整代码

前面一步步拆解完了,最后把所有东西整合到一起。先看一下整体架构:

1graph LR
2    View -->|用户操作| ViewModel
3    ViewModel -->|状态更新| View
4    ViewModel -->|网络请求| APIService
5    APIService -->|响应数据| ViewModel
6
7    style View fill:#E8F5E9,stroke:#4CAF50
8    style ViewModel fill:#E3F2FD,stroke:#2196F3
9    style APIService fill:#FFF3E0,stroke:#FF9800
10

View 只管渲染和转发用户操作,ViewModel 管状态和请求编排,APIService 做实际的网络调用。数据流向是单向的:用户操作 → ViewModel 处理 → APIService 请求 → 数据回来更新状态 → View 自动刷新。

Model

1struct Item: Identifiable, Equatable {
2    let id: String
3    let title: String
4}
5
6struct PageInfo: Equatable {
7    let endCursor: String?
8    let hasNextPage: Bool
9}
10
11struct PagedResponse {
12    let items: [Item]
13    let pageInfo: PageInfo
14}
15

ViewState

1enum ViewState {
2    case initialLoading
3    case loaded
4    case empty
5    case error(String)
6}
7

ViewModel

1@MainActor @Observable
2final class ItemListViewModel {
3    // MARK: - State
4
5    private(set) var items: [Item] = []
6    private(set) var isLoading = false
7    private(set) var error: Error?
8
9    // MARK: - Private
10
11    private let prefetchThreshold = 5
12    private var pageInfo: PageInfo?
13    private var loadTask: Task<Void, Never>?
14
15    // MARK: - Computed
16
17    var canLoadMore: Bool {
18        guard !isLoading else { return false }
19        guard let pageInfo else { return items.isEmpty }
20        return pageInfo.hasNextPage
21    }
22
23    var isEmpty: Bool {
24        !isLoading && items.isEmpty && error == nil && pageInfo != nil
25    }
26
27    var viewState: ViewState {
28        if let error, items.isEmpty {
29            return .error(error.localizedDescription)
30        }
31        if isLoading && items.isEmpty {
32            return .initialLoading
33        }
34        if isEmpty {
35            return .empty
36        }
37        return .loaded
38    }
39
40    // MARK: - Trigger
41
42    func onItemAppear(_ item: Item) {
43        guard let index = items.firstIndex(of: item),
44              index >= items.count - prefetchThreshold else { return }
45        loadNextPage()
46    }
47
48    // MARK: - Actions
49
50    func loadNextPage() {
51        guard canLoadMore else { return }
52        loadTask?.cancel()
53        isLoading = true
54
55        loadTask = Task { [weak self] in
56            guard let self else { return }
57            defer { self.isLoading = false }
58
59            do {
60                let response = try await APIService.fetchItems(after: pageInfo?.endCursor)
61                guard !Task.isCancelled else { return }
62                self.items.append(contentsOf: response.items)
63                self.pageInfo = response.pageInfo
64                self.error = nil
65            } catch is CancellationError {
66                // Task was cancelled, do nothing
67            } catch {
68                guard !Task.isCancelled else { return }
69                self.error = error
70            }
71        }
72    }
73
74    func retry() {
75        error = nil
76        loadNextPage()
77    }
78
79    func reset() {
80        loadTask?.cancel()
81        items = []
82        pageInfo = nil
83        isLoading = false
84        error = nil
85    }
86}
87

View

1struct ItemListView: View {
2    @State private var viewModel = ItemListViewModel()
3
4    var body: some View {
5        Group {
6            switch viewModel.viewState {
7            case .initialLoading:
8                ProgressView()
9            case .empty:
10                ContentUnavailableView("暂无数据", systemImage: "tray")
11            case .error(let message):
12                ErrorView(message: message) { viewModel.retry() }
13            case .loaded:
14                ScrollView {
15                    LazyVStack(spacing: 0) {
16                        ForEach(viewModel.items) { item in
17                            ItemRow(item: item)
18                                .onAppear { viewModel.onItemAppear(item) }
19                        }
20                        loadingFooter
21                    }
22                }
23            }
24        }
25        .task { viewModel.loadNextPage() }
26    }
27
28    @ViewBuilder
29    private var loadingFooter: some View {
30        if viewModel.error != nil {
31            VStack(spacing: 8) {
32                Text("加载失败")
33                    .font(.caption)
34                    .foregroundStyle(.secondary)
35                Button("Retry") { viewModel.retry() }
36                    .buttonStyle(.bordered)
37            }
38            .frame(maxWidth: .infinity)
39            .padding()
40        } else if viewModel.isLoading {
41            ProgressView()
42                .frame(maxWidth: .infinity)
43                .padding()
44        }
45    }
46}
47

总结

回顾一下整个思路:

  1. 从简单方案说起LazyVStack + onAppear last item,先把原理讲清楚
  2. 暴露问题并优化 — 重复请求 → guard;体验停顿 → threshold prefetch
  3. 展示工程素养 — Task 取消、error handling、retry
  4. 完整架构 — View 只渲染 + 转发,ViewModel 管状态 + 编排

SwiftUI 如何实现 Infinite Scroll?》 是转载文章,点击查看原文


相关推荐


基于 Cloudflare 生态的 AI Agent 实现
Surmon2026/3/19

2026 新年的一个夜晚,窗外炮竹烟花争相闪耀,脑海里灵光一闪:我这快十年的老博客能不能也赶一波时髦,实现一个真正「有用」的智能助手? 有用 的意思是,它不能是一个只会随便聊天的机器人,而是一个真正了解我(博主)、了解博客内容的 AI 分身。它最好能事无巨细地知道我写过哪些文章,了解我的观点、立场和经历,能根据访客的问题去知识库里精准地找到最相关的内容,再结合上下文给出自然又富有意义的回答。 它应该是一张鲜活、灵动的个人名片。 这并不是一个多么复杂的需求,开源工具和商业基建也已经很成熟了,但真正


从零开发一个掘金自动发布 Skill,并上架 Clawhub
小巫debug日记2026/3/10

从零开发一个掘金自动发布 Skill,并上架 Clawhub 本文记录了一次完整的 Skill 开发旅程:从一句「帮我创建一个可以自动发布文章到掘金的 skill」开始,到最终成功上架 Clawhub,全程真实还原每一个关键决策和踩坑过程。 背景:为什么要做这个 Skill? 我日常运营一个 AI 资讯账号,每天需要将 Markdown 格式的文章发布到多个平台,包括微信公众号、小红书、掘金等。其中微信公众号和小红书已经有现成的 Skill 可以用,但掘金没有。 每次发布掘金都要: 打开


Word 中 MathType 启动慢、卡顿、卡死 | 由于某种原因,PowerPoint 无法加载MathType……
斐夷所非2026/3/2

注:本文为 “office 中 MathType 启动、加载异常” 相关合辑。 图片清晰度受引文原图所限。 略作重排,如有内容异常,请看原文。 Word 2013 中 MathType 窗口启动延迟问题分析与解决方案 香蕉君达 发布于 2026-02-19 12:12 1 现象描述 通过快捷键或功能区按钮在 Word 2013 中插入公式时,编辑窗口启动延迟时长约为 3~4 秒,对文档编辑流程造成干扰。 测试表明,若系统中已存在至少一个处于打开状态的 MathType 窗口,后续公式


SpringBoot多环境配置实战指南
北极的代码2026/2/22

前言:在之前的开发环境中要跟改配置,测试环境也要改,每次切换环境都要手动修改配置文件 常常发生"我们在本地能运行,怎么部署到服务器就报错"的情况,一不小心就把测试环境的配置提交到代码库。因此我们提出了多环境开发配置。 多环境开发配置: 在SpringBoot中,多环境配置的管理核心是利用Profile机制,它允许我们为不同的运行环境(开发,测试,生产)定义独立的配置,并在应用启动时动态的激活,从而实现配置等隔离与灵活切换。 核心实现方式:Profile 特定配置文件 总之就


聊一聊 CLI:为什么真正的工程能力,都藏在命令行里?
G探险者2026/2/14

大家好,我是G探险者! 今天我们来聊一聊CLI。 在很多人眼里,命令行(CLI,Command Line Interface)是“黑框 + 英文命令”的代名词。 对普通用户来说,它晦涩、难记、不友好。 但对工程师来说—— CLI 是系统可编排能力的起点,是自动化的基础设施,是 DevOps 的地基。 今天我们不从“怎么用命令”讲起,而是聊一聊: CLI 是怎么诞生的? 为什么它没有被 GUI 取代? 为什么所有现代基础设施几乎都优先设计 CLI? 为什么 CLI 是工程能力的分水岭?


你这一生到底该如何赚钱?
袁庭新2026/2/5

大家好,我是袁庭新。 赚钱是每个成年人每天的头等大事,那你有没有认真思考过:你这一辈子到底应该如何赚钱?根据这几年的总结,我认为赚钱的方式无非以下三种: 用时间赚钱 用金钱赚钱 用金钱和时间一起赚钱 这三种赚钱方式的回报是不一样的,它们依次越来越大,最牛的就是用“时间+金钱”赚钱。 我们绝大多数人一生摆脱不了“用时间赚钱”这种模式,想要获得更多回报就低拼命上班加班。但,用时间赚钱的方式是可以改良的,最核心的策略就是“想尽一切办法把自己的同一份时间出售很多次”,举几个例子吧,比如:讲课、写书


爷爷你关注的前端博主复活了!! 他学python去了??
jinzunqinjiu2026/1/27

如何配置python环境。 hello,兄弟们马上过年了,想死你们了。转眼间就已经毕业半年。也工作了快一年了。从实习生一路跌跌撞撞,从刚开始连react的状态依赖都老是写死循环到现在已经经历过很多项目了。说来这一年也有很多成长,参与了公司很多的项目,看过各种代码。最终在ai的加持下已经能够独挡一面。但是最近公司开始掀起了一股ai风,以及网上ai全栈的兴起,我想我是坐不住了。深耕前端 or 技术转型。 小孩子才做选择,前端为主ai为辅,所以我要开始学习python逐渐开始学习ai应用了。正好我也没


【我与2025】裁员、旅游、找工作、媳妇没跑
修己xj2026/1/18

现在是2026年1月下旬。以往的年终总结总被搁置,今年却有些不同——家里添了新成员,自己的心态也悄然变化。于是决定写下这些文字,既是回顾我的2025,也是一次认真的复盘。 裁员 2021年6月,我加入上一家公司,一待就是四年。2025年收到的第一份“礼物”,竟是公司的裁员通知。我负责的是运营业务系统,因为常有线上问题需要处理,所以即便下班后、节假日也离不开电脑。几年来,我几乎没出省旅行过,每次回家都随身带着电脑,随时待命。 刚入职时,公司正处于扩张期,盈利状况很好。没过多久,就搬进了自购的整层


Incremark Solid 版本上线:Vue/React/Svelte/Solid 四大框架,统一体验
king王一帅2026/1/10

Incremark 现已支持 Solid,至此完成了对 Vue、React、Svelte、Solid 四大主流前端框架的全面覆盖。 为什么要做框架无关 市面上大多数 Markdown 渲染库都是针对特定框架开发的。这带来几个问题: 重复造轮子:每个框架社区都在独立实现相似的功能 能力不一致:不同框架的实现质量参差不齐 团队切换成本:换框架意味着重新学习新的 API Incremark 采用不同的思路:核心逻辑与 UI 框架完全解耦。 @incremark/core 负责所有解析、转换、增量更


机器学习数据集完全指南:从公开资源到Sklearn实战
郝学胜-神的一滴2026/1/1

机器学习数据集完全指南:从公开资源到Sklearn实战 1. 引言:为什么数据集如此重要?2. 机器学习公开数据集大全2.1 综合型数据集平台2.2 领域特定数据集 3. Sklearn内置数据集详解3.1 小型玩具数据集3.2 大型真实世界数据集3.3 完整列表 4. Sklearn数据集加载实战4.1 基本加载方法4.2 数据集对象结构4.3 转换为Pandas DataFrame 5. Sklearn数据集处理API大全5.1 数据分割5.2 特征缩放5.3 特征编码5.4

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客