Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >TCA - SwiftUI 的救星?(二)

TCA - SwiftUI 的救星?(二)

作者头像
Swift社区
发布于 2021-12-20 06:08:58
发布于 2021-12-20 06:08:58
1.4K00
代码可运行
举报
文章被收录于专栏:Swift社区Swift社区
运行总次数:0
代码可运行

前言

上一篇关于 TCA 的文章中,我们通过总览的方式看到了 TCA 中一个 Feature 的运作方式,并尝试实现了一个最小的 Feature 和它的测试。在这篇文章中,我们会继续深入,看看 TCA 中对 Binding 的处理,以及使用 Environment 来把依赖从 reducer 中解耦的方法。

如果你想要跟做,可以直接使用上一篇文章完成练习后最后的状态,或者从这里[1]获取到起始代码。

关于绑定

绑定和普通状态的区别

在上一篇文章中,我们实现了“点击按钮” -> “发送 Action” -> “更新 State” -> “触发 UI 更新” 的流程,这解决了“状态驱动 UI”这一课题。不过,除了单纯的“通过状态来更新 UI” 以外,SwiftUI 同时也支持在反方向使用 @Binding 的方式把某个 State 绑定给控件,让 UI 能够不经由我们的代码,来更改某个状态。在 SwiftUI 中,我们几乎可以在所有既表示状态,又能接受输入的控件上找到这种模式,比如 TextField 接受 String 的绑定 Binding<String>Toggle 接受 Bool 的绑定 Binding<Bool> 等。

当我们把某个状态通过 Binding 交给其他 view 时,这个 view 就有能力改变去直接改变状态了,实际上这是违反了 TCA 中关于只能在 reducer 中更改状态的规定的。对于绑定,TCA 中为 View Store 添加了将状态转换为一种“特殊绑定关系”的方法。我们来试试看把 Counter 例子中的显示数字的 Text 改成可以接受直接输入的 TextField

在 TCA 中实现单个绑定

首先,为 CounterActioncounterReducer 添加对应的接受一个字符串值来设定 count 的能力:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
enum CounterAction {
  case increment
  case decrement
+ case setCount(String)
  case reset
}

let counterReducer = Reducer<Counter, CounterAction, CounterEnvironment> {
  state, action, _ in
  switch action {
  // ...
+ case .setCount(let text):
+   if let value = Int(text) {
+     state.count = value
+   }
+   return .none
  // ...
}.debug()

接下来,把 body 中原来的 Text 替换为下面的 TextField

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var body: some View {
  WithViewStore(store) { viewStore in
    // ...
-   Text("\(viewStore.count)")
+   TextField(
+     String(viewStore.count),
+     text: viewStore.binding(
+       get: { String($0.count) },
+       send: { CounterAction.setCount($0) }
+     )
+   )
+     .frame(width: 40)
+     .multilineTextAlignment(.center)
      .foregroundColor(colorOfCount(viewStore.count))
  }
}

viewStore.binding 方法接受 getsend 两个参数,它们都是和当前 View Store 及绑定 view 类型相关的泛型函数。在特化 (将泛型在这个上下文中转换为具体类型) 后:

  • get: (Counter) -> String 负责为对象 View (这里的 TextField) 提供数据。
  • send: (String) -> CounterAction 负责将 View 新发送的值转换为 View Store 可以理解的 action,并发送它来触发 counterReducer。 在 counterReducer 接到 binding 给出的 setCount 事件后,我们就回到使用 reducer 进行状态更新,并驱动 UI 的标准 TCA 循环中了。

传统的 SwiftUI 中,我们在通过 $ 符号获取一个状态的 Binding 时,实际上是调用了它的 projectedValue。而 viewStore.binding 在内部通过将 View Store 自己包装到一个 ObservedObject 里,然后通过自定义的 projectedValue 来把输入的 getsend 设置给 Binding 使用中。对内,它通过内部存储维持了状态,并把这个细节隐藏起来;对外,它通过 action 来把状态的改变发送出去。捕获这个改变,并对应地更新它,最后再把新的状态再次通过 get 设置给 binding,是开发者需要保证的事情。

简化代码

做一点重构:现在 bindingget 是从 $0.count 生成的 String,reducer 中对 state.count 的设定也需要先从 String 转换为 Int。我们把这部分 Mode 和 View 表现形式相关的部分抽取出来,放到 Counter 的一个 extension 中,作为 View Model 使用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
extension Counter {
  var countString: String {
    get { String(count) }
    set { count = Int(newValue) ?? count }
  }
}

把 reducer 中转换 String 的部分替换成 countString

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let counterReducer = Reducer<Counter, CounterAction, CounterEnvironment> {
  state, action, _ in
  switch action {
  // ...
  case .setCount(let text):
-   if let value = Int(text) {
-     state.count = value
-   }
+   state.countString = text
    return .none
  // ...
}.debug()

Swift 5.2 中,KeyPath 已经可以被当作函数使用了,因此我们可以把 \Counter.countString 的类型看作 (Counter) -> String。同时,Swift 5.3 中 enum case 也可以当作函数[2],可以认为 CounterAction.setCount 具有类型 (String) -> CounterAction。两者恰好满足 binding 的两个参数的要求,所以可以进一步将创建绑定的部分简化:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ...
  TextField(
    String(viewStore.count),
    text: viewStore.binding(
-     get: { String($0.count) },
+     get: \.countString,
-     send: { CounterAction.setCount($0) }
+     send: CounterAction.setCount
    )
  )
// ...

最后,别忘了为 .setCount 添加测试!

多个绑定值 如果在一个 Feature 中,有多个绑定值的话,使用例子中这样的方式,每次我们都会需要添加一个 action,然后在 bindingsend 它。这是千篇一律的模板代码,TCA 中设计了 @BindableStateBindableAction,让多个绑定的写法简单一些。具体来说,分三步:

  1. State 中的需要和 UI 绑定的变量添加 @BindableState
  2. Action 声明为 BindableAction,然后添加一个“特殊”的 case binding(BindingAction<Counter>)
  3. 在 Reducer 中处理这个 .binding,并添加 .binding() 调用。

直接用代码说明会更快:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 1
struct MyState: Equatable {
+ @BindableState var foo: Bool = false
+ @BindableState var bar: String = ""
}

// 2
- enum MyAction {
+ enum MyAction: BindableAction {
+   case binding(BindingAction<MyState>)
}

// 3
let myReducer = //...
  // ...
+ case .binding:
+   return .none
}
+ .binding()

这样一番操作后,我们就可以在 View 里用类似标准 SwiftUI 的做法,使用 $ 取 projected value 来进行 Binding 了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct MyView: View {
  let store: Store<MyState, MyAction>
  var body: some View {
    WithViewStore(store) { viewStore in
+     Toggle("Toggle!", isOn: viewStore.binding(\.$foo))
+     TextField("Text Field!", text: viewStore.binding(\.$bar))
    }
  }
}

这样一来,即使有多个 binding 值,我们也只需要用一个 .binding action 就能对应了。这段代码能够工作,是因为 BindableAction 要求一个签名为 BindingAction<State> -> Self 且名为 binding 的函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public protocol BindableAction {
  static func binding(_ action: BindingAction<State>) -> Self
}

再一次,利用了将 enum case 作为函数使用的 Swift 新特性,代码可以变得非常简单优雅。

环境值

猜数字游戏

回到 Counter 的例子来。既然已经有输入数字的方式了,那不如来做一个猜数字的小游戏吧!

猜数字:程序随机选择 -100 到 100 之间的数字,用户输入一个数字,程序判断这个数字是否就是随机选择的数字。如果不是,返回“太大”或者“太小”作为反馈,并要求用户继续尝试输入下一个数字进行猜测。

最简单的方法,是在 Counter 中添加一个属性,用来持有这个随机数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Counter: Equatable {
  var count: Int = 0
+ let secret = Int.random(in: -100 ... 100)
}

检查 countsecret 的关系,返回答案:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
extension Counter {
  enum CheckResult {
    case lower, equal, higher
  }
  
  var checkResult: CheckResult {
    if count < secret { return .lower }
    if count > secret { return .higher }
    return .equal
  }
}

有了这个模型,我们就可以通过使用 checkResult 来在 view 中显示一个代表结果的 Label 了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct CounterView: View {
  let store: Store<Counter, CounterAction>
  var body: some View {
    WithViewStore(store) { viewStore in
      VStack {
+       checkLabel(with: viewStore.checkResult)
        HStack {
          Button("-") { viewStore.send(.decrement) }
          // ...
  }
  
  func checkLabel(with checkResult: Counter.CheckResult) -> some View {
    switch checkResult {
    case .lower:
      return Label("Lower", systemImage: "lessthan.circle")
        .foregroundColor(.red)
    case .higher:
      return Label("Higher", systemImage: "greaterthan.circle")
        .foregroundColor(.red)
    case .equal:
      return Label("Correct", systemImage: "checkmark.circle")
        .foregroundColor(.green)
    }
  }
}

最终,我们可以得到这样的 UI:

外部依赖

当我们用这个 UI “蒙对”答案后,Reset 按钮虽然可以把猜测归零,但它并不能为我们重开一局,这当然有点无聊。我们来试试看把 Reset 按钮改成 New Game 按钮。

在 UI 和 CounterAction 里我们已经定义了 .reset 行为了,进行一些重命名的工作:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
enum CounterAction {
  // ...
- case reset
+ case playNext
}

struct CounterView: View {
  // ...
  var body: some View {
    // ...
-   Button("Reset") { viewStore.send(.reset) }
+   Button("Next") { viewStore.send(.playNext) }
  }
}

然后在 counterReducer 里处理这个情况,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct Counter: Equatable {
  var count: Int = 0
- let secret = Int.random(in: -100 ... 100)
+ var secret = Int.random(in: -100 ... 100)
}

let counterReducer = Reducer<Counter, CounterAction, CounterEnvironment> {
  // ...
- case .reset:
+ case .playNext:
    state.count = 0
+   state.secret = Int.random(in: -100 ... 100)
    return .none
  // ...
}.debug()

运行 app,观察 reducer debug() 的输出,可以看到一切正常!太好了。

随时 Cmd + U 运行测试是大家都应该养成的习惯,这时候我们可以发现测试编译失败了。最后的任务就是修正原来的 .reset 测试,这也很简单:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func testReset() throws {
- store.send(.reset) { state in
+ store.send(.playNext) { state in
    state.count = 0
  }
}

但是,测试的运行结果大概率会失败!

这是因为 .playNext 现在不仅重置 count,也会随机生成新的 secret。而 TestStore 会把 send 闭包结束时的 state 和真正的由 reducer 操作的 state 进行比较并断言:前者没有设置合适的 secret,导致它们并不相等,所以测试失败了。

我们需要一种稳定的方式,来保证测试成功。

使用环境值解决依赖

在 TCA 中,为了保证可测试性,reducer 必须是纯函数:也就是说,相同的输入 (state, action 和 environment) 的组合,必须能给出相同的输入 (在这里输出是 state 和 effect,我们会在后面的文章再接触 effect 角色)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let counterReducer = // ... {

  state, action, _ in 
  // ...
  case .playNext:
    state.count = 0
    state.secret = Int.random(in: -100 ... 100)
    return .none
  //...
}.debug()

在处理 .playNext 时,Int.random 显然无法保证每次调用都给出同样结果,它也是导致 reducer 变得无法测试的原因。TCA 中环境 (Environment) 的概念,就是为了对应这类外部依赖的情况。如果在 reducer 内部出现了依赖外部状态的情况 (比如说这里的 Int.random,使用的是自动选择随机种子的 SystemRandomNumberGenerator),我们可以把这个状态通过 Environment 进行注入,让实际 app 和单元测试能使用不同的环境。

首先,更新 CounterEnvironment,加入一个属性,用它来持有随机生成 Int 的方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct CounterEnvironment {
+ var generateRandom: (ClosedRange<Int>) -> Int
}

现在编译器需要我们为原来 CounterEnvironment() 的地方加上 generateRandom 的设定。我们可以直接在生成时用 Int.random 来创建一个 CounterEnvironment

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
CounterView(
  store: Store(
    initialState: Counter(),
    reducer: counterReducer,
-   environment: CounterEnvironment()
+   environment: CounterEnvironment(
+     generateRandom: { Int.random(in: $0) }
+   )
  )
)

一种更加常见和简洁的做法,是为 CounterEnvironment 定义一组环境,然后把它们传到相应的地方:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct CounterEnvironment {
  var generateRandom: (ClosedRange<Int>) -> Int
  
+ static let live = CounterEnvironment(
+   generateRandom: Int.random
+ )
}

CounterView(
  store: Store(
    initialState: Counter(),
    reducer: counterReducer,
-   environment: CounterEnvironment()
+   environment: .live
  )
)

现在,在 reducer 中,就可以使用注入的环境值来达到和原来等效的结果了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let counterReducer = // ... {
- state, action, _ in
+ state, action, environment in
  // ...
  case .playNext:
    state.count = 0
-   state.secret = Int.random(in: -100 ... 100)
+   state.secret = environment.generateRandom(-100 ... 100)
    return .none
  // ...
}.debug()

万事俱备,回到最开始的目的 - 保证测试能顺利通过。在 test target 中,用类似的方法创建一个 .test 环境:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
extension CounterEnvironment {
  static let test = CounterEnvironment(generateRandom: { _ in 5 })
}

现在,在生成 TestStore 的时候,使用 .test,然后在断言时生成合适的 Counter 作为新的 state,测试就能顺利通过了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
store = TestStore(
  initialState: Counter(count: Int.random(in: -100...100)),
  reducer: counterReducer,
- environment: CounterEnvironment()
+ environment: .test
)

store.send(.playNext) { state in
- state.count = 0
+ state = Counter(count: 0, secret: 5)
}

store.send 的闭包里,我们现在直接为 state 设置了一个新的 Counter,并明确了所有期望的属性。这里也可以分开两行,写成 state.count = 0 以及 state.secret = 5,测试也可以通过。选择哪种方式都可以,但在涉及到复杂的情况下,会倾向于选择完整的赋值:在测试中,我们希望的是通过断言来比较期望 state 和实际 state 的差别,而不是重新去实现一次 reducer 中的逻辑。这可能引入混乱,因为在测试失败时你需要去排查到底是 reducer 本身的问题,还是测试代码中操作状态造成的问题。

其他常见依赖

除了像是 random 系列以外,凡是会随着调用环境的变化 (包括时间,地点,各种外部状态等等) 而打破 reducer 纯函数特性的外部依赖,都应该被纳入 Environment 的范畴。常见的像是 UUID 的生成,当前 Date 的获取,获取某个运行队列 (比如 main queue),使用 Core Location 获取现在的位置信息,负责发送网络请求的网络框架等等。

它们之中有一些是可以同步完成的,比如例子中的 Int.random;有一些则是需要一定时间才能得到结果,比如获取位置信息和发送网络请求。对于后者,我们往往会把它转换为一个 Effect。我们会在下一篇文章中再讨论 Effect

练习

如果你没有跟随本文更新代码,你可以在这里[3]找到下面练习的起始代码。参考实现可以在这里[4]找到。

添加一个 Slider

用键盘和加减号来控制 Counter 已经不错了,但是添加一个 Slider 会更有趣。请为 CounterView 添加一个 Slider,用来来和 TextField 以及 “+” “-“ Button 一起,控制我们的猜数字游戏。

期望的 UI 大概是这样:

别忘了写测试!

完善 Counter,记录更多信息

为了后面功能的开发,我们需要更新一下 Counter 模型。首先,每个谜题添加一些元信息,比如谜题 ID:

在 Counter 中加上下面的属性,然后让它满足 Identifiable

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- struct Counter: Equatable {
+ struct Counter: Equatable, Identifiable {
    var count: Int = 0
    var secret = Int.random(in: -100 ... 100)
  
+   var id: UUID = UUID()
  }

在开始新一轮游戏的时候,记得更新 id。还有,别忘了写测试!

关于我们

Swift社区是由 Swift 爱好者共同维护的公益组织,我们在国内以微信公众号的运营为主,我们会分享以 Swift实战SwiftUlSwift基础为核心的技术内容,也整理收集优秀的学习资料。

特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量,排名不分先后:张安宇@微软戴铭@快手展菲@ESP倪瑶@Trip.com杜鑫瑶@新浪韦弦@Gwell张浩@讯飞张星宇@ByteDance郭英东@便利蜂

参考资料

[1] 起始代码: https://github.com/onevcat/CounterDemo/releases/tag/part-1-finish

[2] enum case 也可以当作函数: https://github.com/apple/swift-evolution/blob/main/proposals/0280-enum-cases-as-protocol-witnesses.md

[3] 起始代码: https://github.com/onevcat/CounterDemo/releases/tag/part-2-start

[4] 参考实现: https://github.com/onevcat/CounterDemo/releases/tag/part-2-finish

- EOF -

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-12-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Swift社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
redis Windows 服务 启动异常,错误1067:进程意外终止。 原
注意这个命令中使用的是 redis.windows-service.conf 这个配置文件
北漂的我
2019/05/29
5.8K0
WLAN AutoConfig 启动报错“错误 1068:依赖服务或组无法启动“
找到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Ndisuio
岫珩
2025/03/05
1.1K0
WLAN AutoConfig 启动报错“错误 1068:依赖服务或组无法启动“
【java报错已解决】redis.clients.jedis.exceptions.JedisConnectionException
在Java开发过程中,与外部服务如Redis进行交互是十分常见的操作。然而,时不时就会遇到一些让人头疼的报错信息,其中就包括“redis.clients.jedis.exceptions.JedisConnectionException”。这个报错一旦出现,往往会让开发者或者环境配置者感到困惑,不知道到底是哪里出了问题,导致程序无法正常与Redis建立连接。那么,今天我们就来深入探讨一下这个报错该如何解决,帮助大家顺利攻克这一难题,让程序能够顺畅地与Redis进行交互。
鸽芷咕
2025/05/29
3050
Windows无法启动MongoDB Server,错误:1053:服务没有及时响应启动或控制请求
安装完mongodb-windows-x86_64-5.0.15-signed.msi时,启动其服务时报错:
I Teach You 我教你
2023/07/18
4.9K0
Windows无法启动MongoDB Server,错误:1053:服务没有及时响应启动或控制请求
linux ssh无法启动的解决方案
Linux上的SSH无法启动,报告/var/empty/sshd must be owned byroot and not group or world-writable的解决方法。
用户1685462
2021/07/27
4.2K0
windows无法启用网络发现的解决方法
WINDOWS无法启用网络发现的解决方法:在搜索局域网计算机时总是提示 “请检查计算机名,或网络线路有问题...” 而打不开局域网上的计算机共享,经检查在“网络和共享中心”-“更改高级共享设置”处 “网络发现”没有启动,但是经过多次尝试 “网络发现”总是无法启动,原因是有一个服务没有启动,即“SSDP Discovery”服务,启动该服务后网络发现 正常启动。 在“运行”对话框中执行“services.msc”命令,在打开的“服务”窗口中找到并双击“SSDP Discovery”系统服务;在弹出的属性对话框中启用此系统服务。
前端皮皮
2020/11/26
1.9K0
【Java报错已解决】redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from th
在Java开发中,当使用Jedis与Redis进行交互时,遇到“redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the”这样的报错,就像在试图打开一扇通往数据宝库的门时却发现钥匙失灵了,让开发者和环境配置者感到困惑。这个异常表明Jedis在获取与Redis的连接资源时遇到了问题,而Redis作为一款高性能的键值对存储数据库,广泛应用于缓存、消息队列、分布式锁等场景,连接问题可能导致这些功能无法正常实现,严重影响整个系统的运行效率和稳定性。因此,深入理解这个报错的产生原因并掌握有效的解决方法,对于保障系统与Redis之间的顺畅通信至关重要。
鸽芷咕
2025/05/29
3310
关于电脑无法开机或无法启动的几种可能和解决方案
问题一:显示器无信号,电源灯亮,主板无报错 VGA接口插错,有独显的情况下不用独显接口而用主板上的接口。--直接换接口就行 VGA线或者显示器电源线没接好--尝试更换VGA线或显示器 问题二:电脑无法
繁花云
2018/07/31
1.6K0
redis启动警告及info查看redis
按照提示分别修复: 1.第一个提示somaxconn这个值为128太小了,这个值是系统的网络连接队列大小,而redis的TCP backlog设置的值为511,因此受限,所以修改下系统的值
周小董
2019/06/22
2K0
网络基础 Windows telnet使用简介及相关问题解决方案
C:\Documents and Settings\Administrator>tlntadmn config port=23
授客
2019/09/11
1.7K0
Windows下MySQL无法启动万能解决方案
window10上安装了MySQL之前使用都是执行net start mysql 启动,执行net stop mysql关闭。 某天开始启动报错“MySQL 服务无法启动”,“请键入 NET HELPMSG 3523 以获得更多的帮助”。
震八方紫面昆仑侠
2020/12/15
3.3K0
Windows下MySQL无法启动万能解决方案
服务器意外断电MySQL无法启动
客户反映无法登录系统。再三询问之下,客户说出一个情况:服务器因信息中心人为原因,最近总是意外断电。更多精彩文章请关注公众号『Pythonnote』或者『全栈技术精选』
小闫同学啊
2021/04/13
7.9K0
Redis安装说明
大多数企业都是基于Linux服务器来部署项目,而且Redis官方也没有提供Windows版本的安装包。因此课程中我们会基于Linux系统来安装Redis.
捞月亮的小北
2023/12/01
4440
Redis安装说明
Linux系统安装Redis遇到的问题及解决
Redis用处很广泛,我不再详细说了,按照这里的教程在Linux上安装Redis,开始了踩坑过程,网上买了一个Linux CentOS 7.3,某云的,巨坑无比啊, Redis 为4.0。
星哥玩云
2022/08/16
2.3K0
Linux系统安装Redis遇到的问题及解决
电商详情页缓存架构(二)环境搭建(搭建CentOS 集群、安装redis及启动方案)
1、virtual box 官网下载最新 (opens new window)(本次笔记使用的是 VirtualBox-6.0.4-128413-Win.exe),安装完成之后,需要先配置下虚拟电脑的默认位置:管理 -> 全局设定 -> 虚拟电脑位置
chenchenchen
2022/03/09
4460
电商详情页缓存架构(二)环境搭建(搭建CentOS 集群、安装redis及启动方案)
Linux(CentOS)自定义镜像(快照)中有数据盘分区重装后服务器无法启动解决方案
假定Linux服务器含有系统盘+数据盘,此时为系统盘做镜像或快照,再用此镜像创建新的服务器时,或为没有数据盘的服务器重装系统时,新的服务器将无法启动。原因是因为原镜像的/etc/fstab中含有数据盘分区信息,新服务器没有,系统启动时会报错,无法通过SSH连接。
参谋带个长
2022/04/27
1.3K0
springboot测试Redis连接,启动之后各种报错的解决方案
养成习惯,先赞后看!!! 这几天在学习Redis,本来其实很简单的,但是测试连接的过程中却遇到了各种各样的问题.这里记录一下,希望能够对你有所帮助.
萌萌哒的瓤瓤
2021/01/13
1.8K0
springboot测试Redis连接,启动之后各种报错的解决方案
BlueHost SSH连接常见错误和解决方法
SSH 为 Secure Shell 的缩写,SSH 是目前较可靠专为远程登录会话和其他网络服务提供安全性的协议。SSH 协议可以有效防止远程管理过程中的信息泄露问题从而达到保障网站的安全性。
主机侦探
2019/09/22
1.5K0
BlueHost SSH连接常见错误和解决方法
Windows Server 2008 R2修改远程桌面连接数
计算机---属性---远程设置---勾选"允许运行任意版本远程桌面的计算机连接(较不安全)"……
云知识Online
2018/05/03
7.1K0
【java报错已解决】com.sun.jersey.api.client.ClientHandlerException配置服务器异常
在Java开发的世界里,与服务器进行交互是许多应用程序的关键环节。然而,这个过程并不总是一帆风顺的,常常会遇到各种各样的报错信息,让人头疼不已。今天我们要聚焦的就是“【java报错已解决】com.sun.jersey.api.client.ClientHandlerException配置服务器异常”这个报错。当遇到此类报错时,开发者或环境配置者往往会陷入困惑,不知道究竟是配置哪里出了问题,又该如何快速有效地解决它。别担心,接下来我们就深入剖析这个报错,为大家提供详细的解决方案,让大家在面对这类问题时能够胸有成竹,轻松应对。
鸽芷咕
2025/05/29
1180
推荐阅读
相关推荐
redis Windows 服务 启动异常,错误1067:进程意外终止。 原
更多 >
LV.2
北京百联咨询有限公司创始人
作者相关精选
交个朋友
加入数据技术工作实战群
获取实战干货 交流技术经验
加入[后端] 腾讯云技术交流站
后端架构设计 高可用系统实现
加入云原生趋势交流群
云原生技术展望 企业上云实践
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验