📖 鸿蒙NEXT开发实战系列 | 第23篇 | 实战篇 🎯 适合人群:有鸿蒙基础的开发者 ⏰ 阅读时间:约20分钟 | 💻 开发环境:DevEco Studio 5.0+


登录注册是每个App的"门面担当",也是用户接触App的第一个交互场景。一个体验良好的登录注册模块,不仅能提升用户留存,还能为后续的功能扩展打下坚实基础。

本文将带你从零实现一个完整的鸿蒙登录注册系统,涵盖表单验证、网络请求封装、Token存储管理、自动登录等核心功能,所有代码均可直接复用到你的项目中。


📑 目录


一、功能架构设计

在开始编码之前,我们先梳理一下整体架构:

┌─────────────────────────────────────────────────────────┐
│                      App启动流程                         │
├─────────────────────────────────────────────────────────┤
│  EntryAbility → 检查本地Token → Token有效? → 首页       │
│                                      ↓ 否               │
│                                   登录页面              │
├─────────────────────────────────────────────────────────┤
│                      登录流程                            │
├─────────────────────────────────────────────────────────┤
│  输入账号密码 → 表单验证 → 发送请求 → 保存Token → 首页   │
├─────────────────────────────────────────────────────────┤
│                      注册流程                            │
├─────────────────────────────────────────────────────────┤
│  输入信息 → 表单验证 → 获取验证码 → 提交注册 → 登录页面  │
└─────────────────────────────────────────────────────────┘

核心模块划分:

模块

职责

关键类

UI层

登录/注册页面展示

LoginPage.ets, RegisterPage.ets

业务层

表单验证、登录逻辑

FormValidator, LoginViewModel

网络层

API请求封装

HttpUtil, ApiConfig

存储层

Token持久化

TokenManager

启动层

自动登录检查

SplashAbility


二、登录页面UI实现

登录页面包含手机号输入、密码输入、登录按钮、注册入口等元素。我们采用ArkUI声明式开发范式来实现。

2.1 登录页面完整代码

// pages/LoginPage.ets
import { router } from '@kit.ArkUI'
import { FormValidator } from '../utils/FormValidator'
import { HttpUtil } from '../utils/HttpUtil'
import { TokenManager } from '../utils/TokenManager'
import { promptAction } from '@kit.ArkUI'

@Entry
@Component
struct LoginPage {
  // 表单数据
  @State phoneNumber: string = ''
  @State password: string = ''
  @State showPassword: boolean = false
  @State isLoading: boolean = false

  // 验证错误提示
  @State phoneError: string = ''
  @State passwordError: string = ''

  // 倒计时相关
  @State countdown: number = 0
  @State isCounting: boolean = false

  // 登录模式:password-密码登录,sms-验证码登录
  @State loginMode: string = 'password'
  @State verifyCode: string = ''
  @State codeError: string = ''

  private timerInterval: number = -1

  aboutToDisappear() {
    // 页面销毁时清除定时器
    if (this.timerInterval !== -1) {
      clearInterval(this.timerInterval)
    }
  }

  build() {
    Column() {
      // 顶部Logo区域
      this.LogoSection()

      // 表单区域
      this.FormSection()

      // 登录按钮
      this.LoginButton()

      // 底部操作
      this.BottomActions()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
    .padding({ left: 32, right: 32 })
  }

  // Logo区域组件
  @Builder
  LogoSection() {
    Column() {
      Image($r('app.media.app_icon'))
        .width(80)
        .height(80)
        .borderRadius(20)
        .margin({ top: 80 })

      Text('欢迎登录')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .margin({ top: 20 })

      Text('登录后享受更多服务')
        .fontSize(14)
        .fontColor('#999999')
        .margin({ top: 8 })
    }
    .margin({ bottom: 40 })
  }

  // 表单区域组件
  @Builder
  FormSection() {
    Column() {
      // 登录模式切换
      Tabs({ barPosition: BarPosition.Start }) {
        TabContent() {
          // 密码登录表单
          this.PasswordLoginForm()
        }
        .tabBar(this.TabBarBuilder('密码登录', 'password'))

        TabContent() {
          // 验证码登录表单
          this.SmsLoginForm()
        }
        .tabBar(this.TabBarBuilder('验证码登录', 'sms'))
      }
      .onChange((index: number) => {
        this.loginMode = index === 0 ? 'password' : 'sms'
        this.clearErrors()
      })
      .barWidth('100%')
      .barMode(BarMode.Fixed)
      .scrollable(false)
      .animationDuration(0)
    }
  }

  @Builder
  TabBarBuilder(title: string, mode: string) {
    Column() {
      Text(title)
        .fontSize(16)
        .fontWeight(this.loginMode === mode ? FontWeight.Bold : FontWeight.Normal)
        .fontColor(this.loginMode === mode ? '#007DFF' : '#666666')
    }
    .padding({ top: 12, bottom: 12 })
  }

  // 密码登录表单
  @Builder
  PasswordLoginForm() {
    Column() {
      // 手机号输入
      Column() {
        Row() {
          Text('+86')
            .fontSize(16)
            .fontColor('#333333')
            .margin({ right: 8 })

          Divider()
            .width(1)
            .height(20)
            .color('#E5E5E5')
            .margin({ right: 8 })

          TextInput({ placeholder: '请输入手机号', text: this.phoneNumber })
            .type(InputType.PhoneNumber)
            .maxLength(11)
            .onChange((value: string) => {
              this.phoneNumber = value
              if (this.phoneError) {
                this.phoneError = ''
              }
            })
            .layoutWeight(1)
            .backgroundColor('transparent')
        }
        .width('100%')
        .height(48)
        .padding({ left: 12, right: 12 })
        .borderRadius(8)
        .border({ width: 1, color: this.phoneError ? '#FF4D4F' : '#E5E5E5' })
        .backgroundColor('#F8F8F8')

        // 手机号错误提示
        if (this.phoneError) {
          Text(this.phoneError)
            .fontSize(12)
            .fontColor('#FF4D4F')
            .margin({ top: 4 })
            .alignSelf(ItemAlign.Start)
        }
      }
      .margin({ bottom: 16 })

      // 密码输入
      Column() {
        Row() {
          TextInput({ placeholder: '请输入密码', text: this.password })
            .type(this.showPassword ? InputType.Normal : InputType.Password)
            .maxLength(20)
            .onChange((value: string) => {
              this.password = value
              if (this.passwordError) {
                this.passwordError = ''
              }
            })
            .layoutWeight(1)
            .backgroundColor('transparent')

          // 密码可见性切换
          Image(this.showPassword ? $r('app.media.ic_eye_open') : $r('app.media.ic_eye_close'))
            .width(20)
            .height(20)
            .fillColor('#999999')
            .onClick(() => {
              this.showPassword = !this.showPassword
            })
        }
        .width('100%')
        .height(48)
        .padding({ left: 12, right: 12 })
        .borderRadius(8)
        .border({ width: 1, color: this.passwordError ? '#FF4D4F' : '#E5E5E5' })
        .backgroundColor('#F8F8F8')

        // 密码错误提示
        if (this.passwordError) {
          Text(this.passwordError)
            .fontSize(12)
            .fontColor('#FF4D4F')
            .margin({ top: 4 })
            .alignSelf(ItemAlign.Start)
        }
      }
      .margin({ bottom: 8 })

      // 忘记密码
      Text('忘记密码?')
        .fontSize(14)
        .fontColor('#007DFF')
        .alignSelf(ItemAlign.End)
        .onClick(() => {
          // 跳转忘记密码页面
          router.pushUrl({ url: 'pages/ForgotPasswordPage' })
        })
    }
    .margin({ top: 20 })
  }

  // 验证码登录表单
  @Builder
  SmsLoginForm() {
    Column() {
      // 手机号输入(复用上面的样式)
      Column() {
        Row() {
          Text('+86')
            .fontSize(16)
            .fontColor('#333333')
            .margin({ right: 8 })

          Divider()
            .width(1)
            .height(20)
            .color('#E5E5E5')
            .margin({ right: 8 })

          TextInput({ placeholder: '请输入手机号', text: this.phoneNumber })
            .type(InputType.PhoneNumber)
            .maxLength(11)
            .onChange((value: string) => {
              this.phoneNumber = value
              if (this.phoneError) {
                this.phoneError = ''
              }
            })
            .layoutWeight(1)
            .backgroundColor('transparent')
        }
        .width('100%')
        .height(48)
        .padding({ left: 12, right: 12 })
        .borderRadius(8)
        .border({ width: 1, color: this.phoneError ? '#FF4D4F' : '#E5E5E5' })
        .backgroundColor('#F8F8F8')

        if (this.phoneError) {
          Text(this.phoneError)
            .fontSize(12)
            .fontColor('#FF4D4F')
            .margin({ top: 4 })
            .alignSelf(ItemAlign.Start)
        }
      }
      .margin({ bottom: 16 })

      // 验证码输入
      Column() {
        Row() {
          TextInput({ placeholder: '请输入验证码', text: this.verifyCode })
            .type(InputType.Number)
            .maxLength(6)
            .onChange((value: string) => {
              this.verifyCode = value
              if (this.codeError) {
                this.codeError = ''
              }
            })
            .layoutWeight(1)
            .backgroundColor('transparent')

          // 发送验证码按钮
          Button(this.isCounting ? `${this.countdown}s后重新获取` : '获取验证码')
            .fontSize(14)
            .fontColor(this.isCounting ? '#999999' : '#007DFF')
            .backgroundColor('transparent')
            .enabled(!this.isCounting)
            .onClick(() => {
              this.sendVerifyCode()
            })
        }
        .width('100%')
        .height(48)
        .padding({ left: 12, right: 12 })
        .borderRadius(8)
        .border({ width: 1, color: this.codeError ? '#FF4D4F' : '#E5E5E5' })
        .backgroundColor('#F8F8F8')

        if (this.codeError) {
          Text(this.codeError)
            .fontSize(12)
            .fontColor('#FF4D4F')
            .margin({ top: 4 })
            .alignSelf(ItemAlign.Start)
        }
      }
    }
    .margin({ top: 20 })
  }

  // 登录按钮
  @Builder
  LoginButton() {
    Button(this.isLoading ? '登录中...' : '登录')
      .width('100%')
      .height(48)
      .fontSize(18)
      .fontWeight(FontWeight.Medium)
      .fontColor('#FFFFFF')
      .backgroundColor('#007DFF')
      .borderRadius(24)
      .margin({ top: 32 })
      .enabled(!this.isLoading)
      .onClick(() => {
        this.handleLogin()
      })
  }

  // 底部操作区域
  @Builder
  BottomActions() {
    Column() {
      // 其他登录方式
      Row() {
        Divider().layoutWeight(1).color('#E5E5E5')
        Text('其他登录方式')
          .fontSize(12)
          .fontColor('#999999')
          .margin({ left: 16, right: 16 })
        Divider().layoutWeight(1).color('#E5E5E5')
      }
      .width('100%')
      .margin({ top: 40 })

      // 第三方登录图标
      Row() {
        this.ThirdPartyButton($r('app.media.ic_wechat'), '微信')
        this.ThirdPartyButton($r('app.media.ic_apple'), 'Apple')
        this.ThirdPartyButton($r('app.media.ic_huawei'), '华为账号')
      }
      .margin({ top: 24 })
      .justifyContent(FlexAlign.SpaceAround)
      .width('60%')

      // 注册入口
      Row() {
        Text('还没有账号?')
          .fontSize(14)
          .fontColor('#999999')
        Text('立即注册')
          .fontSize(14)
          .fontColor('#007DFF')
          .margin({ left: 4 })
          .onClick(() => {
            router.pushUrl({ url: 'pages/RegisterPage' })
          })
      }
      .margin({ top: 32 })
    }
    .layoutWeight(1)
    .justifyContent(FlexAlign.End)
    .margin({ bottom: 40 })
  }

  @Builder
  ThirdPartyButton(icon: Resource, name: string) {
    Column() {
      Image(icon)
        .width(44)
        .height(44)
        .borderRadius(22)
        .border({ width: 1, color: '#E5E5E5' })

      Text(name)
        .fontSize(11)
        .fontColor('#666666')
        .margin({ top: 6 })
    }
    .onClick(() => {
      // 第三方登录逻辑
    })
  }

  // ===================== 业务逻辑 =====================

  // 发送验证码
  async sendVerifyCode() {
    // 验证手机号
    const phoneResult = FormValidator.validatePhone(this.phoneNumber)
    if (!phoneResult.valid) {
      this.phoneError = phoneResult.message
      return
    }

    try {
      const result = await HttpUtil.post('/api/sms/send', {
        phone: this.phoneNumber,
        type: 'login'
      })

      if (result.code === 200) {
        promptAction.showToast({ message: '验证码已发送' })
        this.startCountdown()
      } else {
        promptAction.showToast({ message: result.message || '发送失败' })
      }
    } catch (error) {
      promptAction.showToast({ message: '网络异常,请稍后重试' })
    }
  }

  // 开始倒计时
  startCountdown() {
    this.isCounting = true
    this.countdown = 60

    this.timerInterval = setInterval(() => {
      this.countdown--
      if (this.countdown <= 0) {
        this.isCounting = false
        clearInterval(this.timerInterval)
        this.timerInterval = -1
      }
    }, 1000)
  }

  // 处理登录
  async handleLogin() {
    // 表单验证
    if (!this.validateForm()) {
      return
    }

    this.isLoading = true

    try {
      let params: Record<string, string> = {
        phone: this.phoneNumber
      }

      if (this.loginMode === 'password') {
        params.password = this.password
        params.loginType = 'password'
      } else {
        params.verifyCode = this.verifyCode
        params.loginType = 'sms'
      }

      const result = await HttpUtil.post('/api/user/login', params)

      if (result.code === 200) {
        // 保存Token信息
        const tokenData = result.data
        await TokenManager.saveToken({
          accessToken: tokenData.accessToken,
          refreshToken: tokenData.refreshToken,
          expiresIn: tokenData.expiresIn,
          userId: tokenData.userId
        })

        // 保存用户信息
        await TokenManager.saveUserInfo(tokenData.userInfo)

        promptAction.showToast({ message: '登录成功' })

        // 跳转首页
        router.replaceUrl({ url: 'pages/Index' })
      } else {
        promptAction.showToast({ message: result.message || '登录失败' })
      }
    } catch (error) {
      promptAction.showToast({ message: '网络异常,请稍后重试' })
    } finally {
      this.isLoading = false
    }
  }

  // 表单验证
  validateForm(): boolean {
    let isValid = true

    // 验证手机号
    const phoneResult = FormValidator.validatePhone(this.phoneNumber)
    if (!phoneResult.valid) {
      this.phoneError = phoneResult.message
      isValid = false
    }

    if (this.loginMode === 'password') {
      // 验证密码
      const passwordResult = FormValidator.validatePassword(this.password)
      if (!passwordResult.valid) {
        this.passwordError = passwordResult.message
        isValid = false
      }
    } else {
      // 验证验证码
      const codeResult = FormValidator.validateVerifyCode(this.verifyCode)
      if (!codeResult.valid) {
        this.codeError = codeResult.message
        isValid = false
      }
    }

    return isValid
  }

  // 清除错误信息
  clearErrors() {
    this.phoneError = ''
    this.passwordError = ''
    this.codeError = ''
  }
}

代码要点说明:

  1. Tab切换登录模式:支持密码登录和验证码登录两种方式

  2. 输入框样式:使用圆角边框、聚焦状态颜色变化

  3. 倒计时功能:发送验证码后60秒倒计时,防止频繁发送

  4. 错误提示:输入框下方实时显示验证错误信息


三、注册页面UI实现

注册页面需要收集更多用户信息,包括手机号、验证码、密码、确认密码等。

// pages/RegisterPage.ets
import { router } from '@kit.ArkUI'
import { FormValidator } from '../utils/FormValidator'
import { HttpUtil } from '../utils/HttpUtil'
import { promptAction } from '@kit.ArkUI'

@Entry
@Component
struct RegisterPage {
  @State phoneNumber: string = ''
  @State verifyCode: string = ''
  @State password: string = ''
  @State confirmPassword: string = ''
  @State isAgreed: boolean = false
  @State isLoading: boolean = false

  // 错误信息
  @State phoneError: string = ''
  @State codeError: string = ''
  @State passwordError: string = ''
  @State confirmError: string = ''

  // 倒计时
  @State countdown: number = 0
  @State isCounting: boolean = false

  // 密码强度
  @State passwordStrength: number = 0 // 0:无 1:弱 2:中 3:强

  private timerInterval: number = -1

  aboutToDisappear() {
    if (this.timerInterval !== -1) {
      clearInterval(this.timerInterval)
    }
  }

  build() {
    Column() {
      // 导航栏
      Row() {
        Image($r('app.media.ic_back'))
          .width(24)
          .height(24)
          .onClick(() => router.back())

        Text('注册账号')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ left: 12 })

        Blank()
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })

      // 表单区域
      Scroll() {
        Column() {
          // 手机号输入
          this.InputField('手机号', '请输入手机号', this.phoneNumber, InputType.PhoneNumber, 11, (value) => {
            this.phoneNumber = value
            this.phoneError = ''
          }, this.phoneError)

          // 验证码输入
          Column() {
            Text('验证码')
              .fontSize(14)
              .fontColor('#333333')
              .margin({ bottom: 8 })
              .alignSelf(ItemAlign.Start)

            Row() {
              TextInput({ placeholder: '请输入验证码', text: this.verifyCode })
                .type(InputType.Number)
                .maxLength(6)
                .height(48)
                .layoutWeight(1)
                .backgroundColor('#F8F8F8')
                .borderRadius(8)
                .padding({ left: 12, right: 12 })
                .onChange((value: string) => {
                  this.verifyCode = value
                  this.codeError = ''
                })

              Button(this.isCounting ? `${this.countdown}s` : '获取验证码')
                .width(120)
                .height(48)
                .fontSize(14)
                .fontColor(this.isCounting ? '#999999' : '#007DFF')
                .backgroundColor(this.isCounting ? '#F0F0F0' : '#EBF5FF')
                .borderRadius(8)
                .enabled(!this.isCounting)
                .onClick(() => this.sendVerifyCode())
            }
            .width('100%')

            if (this.codeError) {
              Text(this.codeError)
                .fontSize(12)
                .fontColor('#FF4D4F')
                .margin({ top: 4 })
                .alignSelf(ItemAlign.Start)
            }
          }
          .margin({ top: 20 })

          // 密码输入
          Column() {
            Text('设置密码')
              .fontSize(14)
              .fontColor('#333333')
              .margin({ bottom: 8 })
              .alignSelf(ItemAlign.Start)

            TextInput({ placeholder: '请设置密码(8-20位,含字母和数字)', text: this.password })
              .type(InputType.Password)
              .maxLength(20)
              .height(48)
              .width('100%')
              .backgroundColor('#F8F8F8')
              .borderRadius(8)
              .padding({ left: 12, right: 12 })
              .onChange((value: string) => {
                this.password = value
                this.passwordError = ''
                this.updatePasswordStrength()
              })

            // 密码强度指示器
            Row() {
              ForEach([1, 2, 3], (level: number) => {
                Row()
                  .width('30%')
                  .height(4)
                  .borderRadius(2)
                  .backgroundColor(this.passwordStrength >= level ?
                    (level === 1 ? '#FF4D4F' : level === 2 ? '#FAAD14' : '#52C41A') : '#E5E5E5')
                  .margin({ right: 4 })
              })

              Text(this.getPasswordStrengthText())
                .fontSize(12)
                .fontColor(this.getStrengthColor())
                .margin({ left: 8 })
            }
            .width('100%')
            .margin({ top: 8 })

            if (this.passwordError) {
              Text(this.passwordError)
                .fontSize(12)
                .fontColor('#FF4D4F')
                .margin({ top: 4 })
                .alignSelf(ItemAlign.Start)
            }
          }
          .margin({ top: 20 })

          // 确认密码
          this.InputField('确认密码', '请再次输入密码', this.confirmPassword, InputType.Password, 20, (value) => {
            this.confirmPassword = value
            this.confirmError = ''
          }, this.confirmError)

          // 用户协议
          Row() {
            Checkbox()
              .select(this.isAgreed)
              .selectedColor('#007DFF')
              .onChange((value: boolean) => {
                this.isAgreed = value
              })
              .width(18)
              .height(18)

            Text('我已阅读并同意')
              .fontSize(12)
              .fontColor('#666666')
              .margin({ left: 8 })

            Text('《用户协议》')
              .fontSize(12)
              .fontColor('#007DFF')
              .onClick(() => {
                router.pushUrl({ url: 'pages/UserAgreement' })
              })

            Text('和')
              .fontSize(12)
              .fontColor('#666666')

            Text('《隐私政策》')
              .fontSize(12)
              .fontColor('#007DFF')
              .onClick(() => {
                router.pushUrl({ url: 'pages/PrivacyPolicy' })
              })
          }
          .margin({ top: 24 })
        }
        .padding({ left: 32, right: 32, top: 20 })
      }
      .layoutWeight(1)

      // 注册按钮
      Button(this.isLoading ? '注册中...' : '注册')
        .width('85%')
        .height(48)
        .fontSize(18)
        .fontColor('#FFFFFF')
        .backgroundColor('#007DFF')
        .borderRadius(24)
        .margin({ bottom: 40 })
        .enabled(!this.isLoading)
        .onClick(() => this.handleRegister())
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }

  @Builder
  InputField(label: string, placeholder: string, value: string,
    inputType: InputType, maxLength: number,
    onChange: (value: string) => void, error: string) {
    Column() {
      Text(label)
        .fontSize(14)
        .fontColor('#333333')
        .margin({ bottom: 8 })
        .alignSelf(ItemAlign.Start)

      TextInput({ placeholder: placeholder, text: value })
        .type(inputType)
        .maxLength(maxLength)
        .height(48)
        .width('100%')
        .backgroundColor('#F8F8F8')
        .borderRadius(8)
        .padding({ left: 12, right: 12 })
        .border({ width: 1, color: error ? '#FF4D4F' : 'transparent' })
        .onChange(onChange)

      if (error) {
        Text(error)
          .fontSize(12)
          .fontColor('#FF4D4F')
          .margin({ top: 4 })
          .alignSelf(ItemAlign.Start)
      }
    }
    .margin({ top: 20 })
  }

  // 更新密码强度
  updatePasswordStrength() {
    this.passwordStrength = FormValidator.getPasswordStrength(this.password)
  }

  getPasswordStrengthText(): string {
    const texts = ['', '弱', '中', '强']
    return texts[this.passwordStrength]
  }

  getStrengthColor(): string {
    const colors = ['', '#FF4D4F', '#FAAD14', '#52C41A']
    return colors[this.passwordStrength]
  }

  // 发送验证码
  async sendVerifyCode() {
    const phoneResult = FormValidator.validatePhone(this.phoneNumber)
    if (!phoneResult.valid) {
      this.phoneError = phoneResult.message
      return
    }

    try {
      const result = await HttpUtil.post('/api/sms/send', {
        phone: this.phoneNumber,
        type: 'register'
      })

      if (result.code === 200) {
        promptAction.showToast({ message: '验证码已发送' })
        this.startCountdown()
      } else {
        promptAction.showToast({ message: result.message || '发送失败' })
      }
    } catch (error) {
      promptAction.showToast({ message: '网络异常' })
    }
  }

  startCountdown() {
    this.isCounting = true
    this.countdown = 60
    this.timerInterval = setInterval(() => {
      this.countdown--
      if (this.countdown <= 0) {
        this.isCounting = false
        clearInterval(this.timerInterval)
      }
    }, 1000)
  }

  // 处理注册
  async handleRegister() {
    if (!this.validateForm()) {
      return
    }

    if (!this.isAgreed) {
      promptAction.showToast({ message: '请先同意用户协议' })
      return
    }

    this.isLoading = true

    try {
      const result = await HttpUtil.post('/api/user/register', {
        phone: this.phoneNumber,
        verifyCode: this.verifyCode,
        password: this.password
      })

      if (result.code === 200) {
        promptAction.showToast({ message: '注册成功,请登录' })
        router.back()
      } else {
        promptAction.showToast({ message: result.message || '注册失败' })
      }
    } catch (error) {
      promptAction.showToast({ message: '网络异常' })
    } finally {
      this.isLoading = false
    }
  }

  validateForm(): boolean {
    let isValid = true

    const phoneResult = FormValidator.validatePhone(this.phoneNumber)
    if (!phoneResult.valid) {
      this.phoneError = phoneResult.message
      isValid = false
    }

    const codeResult = FormValidator.validateVerifyCode(this.verifyCode)
    if (!codeResult.valid) {
      this.codeError = codeResult.message
      isValid = false
    }

    const passwordResult = FormValidator.validatePassword(this.password)
    if (!passwordResult.valid) {
      this.passwordError = passwordResult.message
      isValid = false
    }

    if (this.password !== this.confirmPassword) {
      this.confirmError = '两次输入的密码不一致'
      isValid = false
    }

    return isValid
  }
}

四、表单验证工具类

封装通用的表单验证逻辑,方便在多个页面复用。

// utils/FormValidator.ets

/**
 * 表单验证工具类
 * 提供手机号、密码、验证码等常用验证功能
 */
export class FormValidator {

  /**
   * 验证手机号格式
   * @param phone 手机号
   * @returns 验证结果
   */
  static validatePhone(phone: string): ValidationResult {
    if (!phone || phone.trim().length === 0) {
      return { valid: false, message: '请输入手机号' }
    }

    // 去除空格
    phone = phone.replace(/\s/g, '')

    // 验证是否为11位数字
    if (!/^1\d{10}$/.test(phone)) {
      return { valid: false, message: '请输入正确的手机号' }
    }

    return { valid: true, message: '' }
  }

  /**
   * 验证密码强度
   * @param password 密码
   * @returns 验证结果
   */
  static validatePassword(password: string): ValidationResult {
    if (!password || password.length === 0) {
      return { valid: false, message: '请输入密码' }
    }

    if (password.length < 8) {
      return { valid: false, message: '密码长度不能少于8位' }
    }

    if (password.length > 20) {
      return { valid: false, message: '密码长度不能超过20位' }
    }

    // 检查是否包含字母和数字
    const hasLetter = /[a-zA-Z]/.test(password)
    const hasNumber = /[0-9]/.test(password)

    if (!hasLetter || !hasNumber) {
      return { valid: false, message: '密码需包含字母和数字' }
    }

    // 检查是否包含特殊字符(可选要求)
    // const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password)
    // if (!hasSpecial) {
    //   return { valid: false, message: '密码需包含特殊字符' }
    // }

    return { valid: true, message: '' }
  }

  /**
   * 获取密码强度等级
   * @param password 密码
   * @returns 强度等级 0-3
   */
  static getPasswordStrength(password: string): number {
    if (!password || password.length < 6) {
      return 0
    }

    let strength = 0

    // 长度检查
    if (password.length >= 8) strength++
    if (password.length >= 12) strength++

    // 包含小写字母
    if (/[a-z]/.test(password)) strength++

    // 包含大写字母
    if (/[A-Z]/.test(password)) strength++

    // 包含数字
    if (/[0-9]/.test(password)) strength++

    // 包含特殊字符
    if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength++

    // 映射到1-3等级
    if (strength <= 2) return 1  // 弱
    if (strength <= 4) return 2  // 中
    return 3                    // 强
  }

  /**
   * 验证验证码
   * @param code 验证码
   * @returns 验证结果
   */
  static validateVerifyCode(code: string): ValidationResult {
    if (!code || code.trim().length === 0) {
      return { valid: false, message: '请输入验证码' }
    }

    if (!/^\d{4,6}$/.test(code)) {
      return { valid: false, message: '请输入正确的验证码' }
    }

    return { valid: true, message: '' }
  }

  /**
   * 验证邮箱格式
   * @param email 邮箱
   * @returns 验证结果
   */
  static validateEmail(email: string): ValidationResult {
    if (!email || email.trim().length === 0) {
      return { valid: false, message: '请输入邮箱' }
    }

    const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
    if (!emailRegex.test(email)) {
      return { valid: false, message: '请输入正确的邮箱地址' }
    }

    return { valid: true, message: '' }
  }

  /**
   * 验证用户名
   * @param username 用户名
   * @returns 验证结果
   */
  static validateUsername(username: string): ValidationResult {
    if (!username || username.trim().length === 0) {
      return { valid: false, message: '请输入用户名' }
    }

    if (username.length < 2 || username.length > 20) {
      return { valid: false, message: '用户名长度为2-20个字符' }
    }

    // 只允许字母、数字、下划线、中文
    const usernameRegex = /^[\u4e00-\u9fa5a-zA-Z0-9_]+$/
    if (!usernameRegex.test(username)) {
      return { valid: false, message: '用户名只能包含字母、数字、下划线和中文' }
    }

    return { valid: true, message: '' }
  }
}

/**
 * 验证结果接口
 */
export interface ValidationResult {
  valid: boolean
  message: string
}

验证规则说明:

字段

规则

示例

手机号

1开头的11位数字

13812345678

密码

8-20位,包含字母和数字

Abc123456

验证码

4-6位纯数字

123456

邮箱

标准邮箱格式

test@example.com


五、网络请求封装

封装统一的网络请求工具类,处理请求拦截、响应拦截、错误处理等。

// utils/HttpUtil.ets
import { http } from '@kit.NetworkKit'
import { TokenManager } from './TokenManager'
import { router } from '@kit.ArkUI'

// API基础配置
const BASE_URL = 'https://api.your-app.com'
const TIMEOUT = 15000 // 15秒超时

/**
 * 网络请求工具类
 */
export class HttpUtil {

  /**
   * 发送GET请求
   */
  static async get(url: string, params?: Record<string, string>): Promise<ApiResponse> {
    const fullUrl = params ? `${url}?${HttpUtil.buildQueryString(params)}` : url
    return HttpUtil.request(fullUrl, 'GET')
  }

  /**
   * 发送POST请求
   */
  static async post(url: string, data?: Record<string, string | number | boolean>): Promise<ApiResponse> {
    return HttpUtil.request(url, 'POST', data)
  }

  /**
   * 发送PUT请求
   */
  static async put(url: string, data?: Record<string, string | number | boolean>): Promise<ApiResponse> {
    return HttpUtil.request(url, 'PUT', data)
  }

  /**
   * 发送DELETE请求
   */
  static async delete(url: string): Promise<ApiResponse> {
    return HttpUtil.request(url, 'DELETE')
  }

  /**
   * 核心请求方法
   */
  private static async request(
    url: string,
    method: string,
    data?: Record<string, string | number | boolean>
  ): Promise<ApiResponse> {
    // 创建HTTP请求实例
    const httpRequest = http.createHttp()

    try {
      // 构建完整URL
      const fullUrl = url.startsWith('http') ? url : `${BASE_URL}${url}`

      // 获取Token并添加到请求头
      const token = await TokenManager.getAccessToken()
      const headers: Record<string, string> = {
        'Content-Type': 'application/json'
      }

      if (token) {
        headers['Authorization'] = `Bearer ${token}`
      }

      // 发送请求
      const response = await httpRequest.request(fullUrl, {
        method: method as http.RequestMethod,
        header: headers,
        extraData: data ? JSON.stringify(data) : undefined,
        connectTimeout: TIMEOUT,
        readTimeout: TIMEOUT
      })

      // 解析响应
      const result: ApiResponse = JSON.parse(response.result as string)

      // 处理Token过期
      if (result.code === 401) {
        // 尝试刷新Token
        const refreshSuccess = await HttpUtil.refreshToken()
        if (refreshSuccess) {
          // 重新发起原请求
          return HttpUtil.request(url, method, data)
        } else {
          // Token刷新失败,跳转登录页
          HttpUtil.handleUnauthorized()
        }
      }

      return result
    } catch (error) {
      console.error('HTTP请求错误:', error)
      return {
        code: -1,
        message: '网络请求失败,请检查网络连接',
        data: null
      }
    } finally {
      // 释放请求资源
      httpRequest.destroy()
    }
  }

  /**
   * 刷新Token
   */
  private static async refreshToken(): Promise<boolean> {
    try {
      const refreshToken = await TokenManager.getRefreshToken()
      if (!refreshToken) {
        return false
      }

      const httpRequest = http.createHttp()
      const response = await httpRequest.request(`${BASE_URL}/api/token/refresh`, {
        method: http.RequestMethod.POST,
        header: {
          'Content-Type': 'application/json'
        },
        extraData: JSON.stringify({ refreshToken }),
        connectTimeout: TIMEOUT,
        readTimeout: TIMEOUT
      })

      httpRequest.destroy()

      const result: ApiResponse = JSON.parse(response.result as string)

      if (result.code === 200 && result.data) {
        // 保存新的Token
        await TokenManager.saveToken({
          accessToken: result.data.accessToken,
          refreshToken: result.data.refreshToken,
          expiresIn: result.data.expiresIn,
          userId: result.data.userId
        })
        return true
      }

      return false
    } catch (error) {
      console.error('Token刷新失败:', error)
      return false
    }
  }

  /**
   * 处理未授权(401)情况
   */
  private static handleUnauthorized() {
    // 清除本地Token
    TokenManager.clearToken()

    // 跳转到登录页
    router.replaceUrl({ url: 'pages/LoginPage' })
  }

  /**
   * 构建查询字符串
   */
  private static buildQueryString(params: Record<string, string>): string {
    return Object.entries(params)
      .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
      .join('&')
  }
}

/**
 * API响应接口
 */
export interface ApiResponse {
  code: number
  message: string
  data: any
}

/**
 * API接口配置
 */
export class ApiConfig {
  // 用户相关
  static readonly USER_LOGIN = '/api/user/login'
  static readonly USER_REGISTER = '/api/user/register'
  static readonly USER_LOGOUT = '/api/user/logout'
  static readonly USER_INFO = '/api/user/info'
  static readonly USER_UPDATE = '/api/user/update'

  // 短信相关
  static readonly SMS_SEND = '/api/sms/send'

  // Token相关
  static readonly TOKEN_REFRESH = '/api/token/refresh'
}

网络请求封装要点:

  1. 统一请求头:自动添加Token到请求头

  2. Token自动刷新:401时自动尝试刷新Token

  3. 错误处理:统一的错误返回格式

  4. 资源释放:请求完成后销毁HTTP实例


六、Token存储管理

使用Preferences实现Token的本地持久化存储。

// utils/TokenManager.ets
import { preferences } from '@kit.ArkData'

// 存储Key常量
const PREF_NAME = 'user_prefs'
const KEY_ACCESS_TOKEN = 'access_token'
const KEY_REFRESH_TOKEN = 'refresh_token'
const KEY_EXPIRES_IN = 'expires_in'
const KEY_TOKEN_SAVE_TIME = 'token_save_time'
const KEY_USER_ID = 'user_id'
const KEY_USER_INFO = 'user_info'

/**
 * Token存储管理类
 */
export class TokenManager {

  /**
   * 获取Preferences实例
   */
  private static async getPreferences(): Promise<preferences.Preferences> {
    const context = getContext(this)
    return preferences.getPreferences(context, PREF_NAME)
  }

  /**
   * 保存Token信息
   * @param tokenData Token数据
   */
  static async saveToken(tokenData: TokenData): Promise<void> {
    try {
      const prefs = await TokenManager.getPreferences()

      await prefs.put(KEY_ACCESS_TOKEN, tokenData.accessToken)
      await prefs.put(KEY_REFRESH_TOKEN, tokenData.refreshToken)
      await prefs.put(KEY_EXPIRES_IN, tokenData.expiresIn.toString())
      await prefs.put(KEY_TOKEN_SAVE_TIME, Date.now().toString())
      await prefs.put(KEY_USER_ID, tokenData.userId)

      await prefs.flush()
    } catch (error) {
      console.error('保存Token失败:', error)
    }
  }

  /**
   * 获取AccessToken
   */
  static async getAccessToken(): Promise<string> {
    try {
      const prefs = await TokenManager.getPreferences()
      const token = await prefs.get(KEY_ACCESS_TOKEN, '') as string
      return token
    } catch (error) {
      console.error('获取Token失败:', error)
      return ''
    }
  }

  /**
   * 获取RefreshToken
   */
  static async getRefreshToken(): Promise<string> {
    try {
      const prefs = await TokenManager.getPreferences()
      return await prefs.get(KEY_REFRESH_TOKEN, '') as string
    } catch (error) {
      console.error('获取RefreshToken失败:', error)
      return ''
    }
  }

  /**
   * 获取用户ID
   */
  static async getUserId(): Promise<string> {
    try {
      const prefs = await TokenManager.getPreferences()
      return await prefs.get(KEY_USER_ID, '') as string
    } catch (error) {
      console.error('获取用户ID失败:', error)
      return ''
    }
  }

  /**
   * 检查Token是否有效
   * @returns Token是否有效
   */
  static async isTokenValid(): Promise<boolean> {
    try {
      const prefs = await TokenManager.getPreferences()

      const accessToken = await prefs.get(KEY_ACCESS_TOKEN, '') as string
      if (!accessToken) {
        return false
      }

      const expiresIn = Number(await prefs.get(KEY_EXPIRES_IN, '0'))
      const saveTime = Number(await prefs.get(KEY_TOKEN_SAVE_TIME, '0'))

      if (saveTime === 0) {
        return false
      }

      // 计算Token是否过期
      // 提前5分钟判定过期,留出刷新时间
      const now = Date.now()
      const expireTime = saveTime + (expiresIn * 1000) - (5 * 60 * 1000)

      return now < expireTime
    } catch (error) {
      console.error('检查Token有效性失败:', error)
      return false
    }
  }

  /**
   * 保存用户信息
   * @param userInfo 用户信息
   */
  static async saveUserInfo(userInfo: object): Promise<void> {
    try {
      const prefs = await TokenManager.getPreferences()
      await prefs.put(KEY_USER_INFO, JSON.stringify(userInfo))
      await prefs.flush()
    } catch (error) {
      console.error('保存用户信息失败:', error)
    }
  }

  /**
   * 获取用户信息
   * @returns 用户信息对象
   */
  static async getUserInfo<T>(): Promise<T | null> {
    try {
      const prefs = await TokenManager.getPreferences()
      const userInfoStr = await prefs.get(KEY_USER_INFO, '') as string
      return userInfoStr ? JSON.parse(userInfoStr) as T : null
    } catch (error) {
      console.error('获取用户信息失败:', error)
      return null
    }
  }

  /**
   * 清除所有Token和用户信息
   */
  static async clearToken(): Promise<void> {
    try {
      const prefs = await TokenManager.getPreferences()

      await prefs.delete(KEY_ACCESS_TOKEN)
      await prefs.delete(KEY_REFRESH_TOKEN)
      await prefs.delete(KEY_EXPIRES_IN)
      await prefs.delete(KEY_TOKEN_SAVE_TIME)
      await prefs.delete(KEY_USER_ID)
      await prefs.delete(KEY_USER_INFO)

      await prefs.flush()
    } catch (error) {
      console.error('清除Token失败:', error)
    }
  }

  /**
   * 检查是否已登录
   * @returns 是否已登录
   */
  static async isLoggedIn(): Promise<boolean> {
    const accessToken = await TokenManager.getAccessToken()
    return !!accessToken
  }
}

/**
 * Token数据接口
 */
export interface TokenData {
  accessToken: string
  refreshToken: string
  expiresIn: number  // Token有效期(秒)
  userId: string
}

/**
 * 用户信息接口(示例)
 */
export interface UserInfo {
  userId: string
  nickname: string
  avatar: string
  phone: string
}

Token管理要点:

功能

实现方式

说明

存储

Preferences

轻量级键值对存储,适合存储Token

过期判断

时间戳计算

保存获取时间,与expiresIn对比

提前刷新

5分钟缓冲

Token过期前5分钟判定为过期

清除

删除所有相关Key

退出登录时调用


七、自动登录实现

在App启动时自动检查Token有效性,实现无感登录。

7.1 启动页面实现

// pages/SplashPage.ets
import { router } from '@kit.ArkUI'
import { TokenManager } from '../utils/TokenManager'
import { HttpUtil } from '../utils/HttpUtil'
import { window } from '@kit.ArkUI'

@Entry
@Component
struct SplashPage {
  @State showLogo: boolean = false
  @State progressText: string = '正在启动...'

  async aboutToAppear() {
    // 设置沉浸式状态栏
    this.setImmersiveStatusBar()

    // 显示Logo动画
    setTimeout(() => {
      this.showLogo = true
    }, 100)
  }

  async onPageShow() {
    // 延迟一下,让用户看到启动页
    await this.delay(1500)

    // 检查登录状态
    await this.checkLoginStatus()
  }

  build() {
    Stack() {
      // 背景
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('#007DFF')

      // Logo和文字
      Column() {
        Image($r('app.media.app_icon'))
          .width(100)
          .height(100)
          .borderRadius(24)
          .opacity(this.showLogo ? 1 : 0)
          .scale({ x: this.showLogo ? 1 : 0.5, y: this.showLogo ? 1 : 0.5 })
          .animation({
            duration: 500,
            curve: Curve.EaseOut
          })

        Text('Your App Name')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
          .margin({ top: 16 })
          .opacity(this.showLogo ? 1 : 0)
          .animation({
            duration: 500,
            curve: Curve.EaseOut,
            delay: 200
          })

        Text(this.progressText)
          .fontSize(14)
          .fontColor('#FFFFFF99')
          .margin({ top: 40 })
      }
    }
    .width('100%')
    .height('100%')
  }

  /**
   * 检查登录状态
   */
  async checkLoginStatus() {
    try {
      this.progressText = '检查登录状态...'

      // 检查本地是否有Token
      const hasToken = await TokenManager.isLoggedIn()

      if (!hasToken) {
        // 未登录,跳转登录页
        this.navigateToLogin()
        return
      }

      // 检查Token是否过期
      const isValid = await TokenManager.isTokenValid()

      if (isValid) {
        // Token有效,尝试获取用户信息验证
        this.progressText = '验证用户信息...'
        const userInfoValid = await this.verifyUserInfo()

        if (userInfoValid) {
          // 登录有效,跳转首页
          this.navigateToHome()
        } else {
          // 用户信息验证失败,跳转登录页
          this.navigateToLogin()
        }
      } else {
        // Token已过期,尝试刷新
        this.progressText = '正在刷新登录状态...'
        const refreshSuccess = await this.refreshToken()

        if (refreshSuccess) {
          this.navigateToHome()
        } else {
          // 刷新失败,需要重新登录
          await TokenManager.clearToken()
          this.navigateToLogin()
        }
      }
    } catch (error) {
      console.error('检查登录状态失败:', error)
      this.navigateToLogin()
    }
  }

  /**
   * 验证用户信息是否有效
   */
  async verifyUserInfo(): Promise<boolean> {
    try {
      const result = await HttpUtil.get('/api/user/info')
      return result.code === 200
    } catch (error) {
      return false
    }
  }

  /**
   * 刷新Token
   */
  async refreshToken(): Promise<boolean> {
    try {
      const refreshToken = await TokenManager.getRefreshToken()
      if (!refreshToken) {
        return false
      }

      const result = await HttpUtil.post('/api/token/refresh', {
        refreshToken
      })

      if (result.code === 200 && result.data) {
        // 保存新的Token
        await TokenManager.saveToken({
          accessToken: result.data.accessToken,
          refreshToken: result.data.refreshToken,
          expiresIn: result.data.expiresIn,
          userId: result.data.userId
        })
        return true
      }

      return false
    } catch (error) {
      console.error('刷新Token失败:', error)
      return false
    }
  }

  /**
   * 跳转到登录页
   */
  navigateToLogin() {
    router.replaceUrl({ url: 'pages/LoginPage' })
  }

  /**
   * 跳转到首页
   */
  navigateToHome() {
    router.replaceUrl({ url: 'pages/Index' })
  }

  /**
   * 设置沉浸式状态栏
   */
  setImmersiveStatusBar() {
    const windowClass = window.getMainWindowSync(getContext(this))
    windowClass.setWindowLayoutFullScreen(true)
  }

  /**
   * 延迟函数
   */
  delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
}

7.2 EntryAbility配置

// entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { window } from '@kit.ArkUI'

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate')
  }

  onDestroy() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy')
  }

  onWindowStageCreate(windowStage: window.WindowStage) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate')

    // 设置启动页
    windowStage.loadContent('pages/SplashPage', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err))
        return
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.')
    })
  }

  onWindowStageDestroy() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy')
  }

  onForeground() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground')
  }

  onBackground() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground')
  }
}

7.3 自动登录流程图

App启动
    │
    ▼
┌───────────────┐
│ 检查本地Token  │
└───────┬───────┘
        │
        ▼
    有Token? ──否──→ 跳转登录页
        │
       是
        ▼
┌───────────────┐
│ 检查Token是否  │
│ 过期           │
└───────┬───────┘
        │
        ▼
    未过期? ──否──→ 尝试刷新Token ──失败──→ 跳转登录页
        │                    │
       是                   成功
        ▼                    ▼
┌───────────────┐     ┌───────────────┐
│ 验证用户信息   │     │   跳转首页     │
└───────┬───────┘     └───────────────┘
        │
        ▼
    有效? ──否──→ 跳转登录页
        │
       是
        ▼
   跳转首页

八、完整登录流程串联

8.1 登录成功后的数据流

// 示例:登录成功后的完整处理流程

// 1. 登录请求
const loginResult = await HttpUtil.post(ApiConfig.USER_LOGIN, {
  phone: phoneNumber,
  password: password
})

if (loginResult.code === 200) {
  // 2. 保存Token
  await TokenManager.saveToken({
    accessToken: loginResult.data.accessToken,
    refreshToken: loginResult.data.refreshToken,
    expiresIn: loginResult.data.expiresIn,
    userId: loginResult.data.userId
  })

  // 3. 保存用户信息
  await TokenManager.saveUserInfo(loginResult.data.userInfo)

  // 4. 跳转首页
  router.replaceUrl({ url: 'pages/Index' })
}

8.2 退出登录实现

// 示例:退出登录

async logout() {
  try {
    // 1. 调用后端退出接口(可选)
    await HttpUtil.post(ApiConfig.USER_LOGOUT)
  } catch (error) {
    console.error('退出登录接口调用失败:', error)
  } finally {
    // 2. 清除本地Token和用户信息
    await TokenManager.clearToken()

    // 3. 跳转登录页
    router.replaceUrl({ url: 'pages/LoginPage' })
  }
}

8.3 网络请求中携带Token示例

// 在HttpUtil中,Token会自动添加到请求头
// 开发者只需这样调用:

// 获取用户信息
const userInfo = await HttpUtil.get(ApiConfig.USER_INFO)

// 更新用户资料
const updateResult = await HttpUtil.put(ApiConfig.USER_UPDATE, {
  nickname: '新昵称',
  avatar: 'https://example.com/avatar.jpg'
})

// 所有请求都会自动携带Token,无需手动添加

九、总结与最佳实践

9.1 本文实现的功能清单

功能模块

实现内容

状态

登录页面

密码登录、验证码登录

已完成

注册页面

手机号注册、密码强度指示

已完成

表单验证

手机号、密码、验证码验证

已完成

网络请求

统一封装、Token自动携带

已完成

Token管理

存储、过期判断、自动刷新

已完成

自动登录

启动检查、无感登录

已完成

9.2 安全最佳实践

  1. 密码安全

    • 前端仅做格式验证,不做加密

    • 建议后端使用bcrypt等算法存储密码

    • 传输时使用HTTPS加密

  2. Token安全

    • AccessToken有效期建议设为2小时

    • RefreshToken有效期建议设为30天

    • 存储在Preferences中,不建议存储在内存

  3. 接口安全

    • 所有接口使用HTTPS

    • 敏感接口添加验证码校验

    • 实现接口防重放攻击

9.3 性能优化建议

  1. 减少网络请求

    • 用户信息本地缓存

    • Token有效期内不重复验证

  2. 优化启动速度

    • SplashPage使用预加载

    • 非关键资源延迟加载

  3. 内存优化

    • 及时销毁HTTP实例

    • 页面退出时清除定时器

9.4 常见问题及解决方案

问题

原因

解决方案

Token频繁失效

服务器时间不同步

增加时间容错,提前刷新

验证码发送失败

手机号格式错误

前端严格验证

自动登录失败

网络超时

增加重试机制

密码强度不够

规则不明确

添加强度指示器

9.5 后续扩展方向

  • 生物识别登录(指纹、人脸)

  • 第三方登录(微信、华为账号)

  • 多账号切换

  • 账号注销功能

  • 登录设备管理


参考资料


📝 下篇预告:第24篇将讲解鸿蒙App的推送通知实现,包括本地通知和远程推送的完整方案。


标签登录注册 Token管理 表单验证 鸿蒙实战 网络请求 ArkUI HarmonyOS NEXT


📖 鸿蒙NEXT开发实战系列 持续更新中,欢迎点赞收藏关注!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐