鸿蒙开发(十三)实战训练:账号注册、登录——网络请求及响应处理

上一节我们已经把基础的界面搭建和主动跳转给写完了,这一节我们就来把网络请求和处理的逻辑给补全。

首先我们需要在module.json5申请网络权限:

module.json5 复制代码
{
  "module": {
    "requestPermissions": [
      {"name": "ohos.permission.INTERNET"}
    ],
    ...
  }
}

我们使用账号密码登录之后会获取到token,我们在EntryAbility.ets中对token进行持久化:

javascript 复制代码
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

  ...

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/Welcome', (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.');
      
      // 对token进行持久化
      PersistentStorage.persistProp('token', '');
    });
  }
  
  ...
}

我们把网络请求会用到的数据结构IUserInfo定义到专门的文件中。ets目录下新建一个models目录(该目录专门用来存放需要复用的各种数据结构),然后在该目录中新建文件UserInfoModel.ets:

UserInfoModel.ets 复制代码
export interface IUserInfo {
  userID?: number,
  account?: string,
  password?: string,
  nickname?: string,
  signature?: string,
  token?: string,
}

由于我们在多个页面中都会用到网络请求,所以我们封装一个自己的rcp工具。ets目录下新建一个utils目录(该目录专门用来存放各种自定义工具),然后在该目录中新建文件MyRcp.ets:

MyRcp.ets 复制代码
import { rcp } from '@kit.RemoteCommunicationKit'
import { promptAction } from '@kit.ArkUI';

// 以程序逻辑的方式获取单向绑定的持久化数据token
let token: string = AppStorage.prop<string>('token').get();

// 实现Interceptor接口,以实现拦截器
class MyInterceptor implements rcp.Interceptor {
  // 实现intercept方法
  async intercept(context: rcp.RequestContext, next: rcp.RequestHandler): Promise<rcp.Response> {
    // 获取请求的content
    let content = context.request.content;
    // 判断是否存在content或者content中是否有password
    // 如果不是,说明是注册或者登录以外的请求,自动设置Header并添加token
    if (!content || content?.toString().search('password') == -1) {
      // 构建新header并添加token
      let headers: rcp.RequestHeaders = {
        authorization: token
      }
      // 设置header
      context.request.headers = headers;
    }

    // 将修改后的请求上下文交个拦截器链中的下一位进行处理,如已是末位则直接投递请求获取响应
    let response = await next.handle(context);
    return response;
  }
}

// 会话配置
const sessionCfg: rcp.SessionConfiguration = {
  // 设置基地址(注意这里需要修改成自己机器在局域网中的地址,不能使用localhost或者127.0.0.1)
  baseAddress: 'http://192.168.3.6:8080',
  // 设置拦截器链
  interceptors: [new MyInterceptor()]
}

// 创建账号操作专用会话,一个会话可以发送多个请求
const accountSession = rcp.createSession(sessionCfg);

// 封装自己的post方法
export async function myPost(url: string, content?: string): Promise<string> {
  // 创建请求
  const req = new rcp.Request(url, 'POST', undefined, content);
  // 调用fetch()发送请求
  const result = await accountSession.fetch(req).catch((err: Error) => {
    promptAction.showDialog({ message: err.message });
  })

  return (result as rcp.Response).toString() as string;
}

我们来到注册页面,编写一个成员方法register()并在点击注册按钮时调用,别忘了判断注册数据合法性:

Register.ets 复制代码
import { IUserInfo } from "../models/UserInfoModel"
import { myPost } from "../utils/myRpc"
import { promptAction } from "@kit.ArkUI"

@Entry
@Component
struct Register {
  ...

  async register() {
    if (this.password != this.againPassword) {
      promptAction.showDialog({
        message: '两次密码输入不一致!'
      });
      return;
    }
    if (this.account == '' || this.nickname == '' || this.password == '') {
      promptAction.showDialog({
        message: '账号/昵称/密码不能为空!'
      });
      return;
    }

    // 数据合法,进行注册    
    let userInfo: IUserInfo = {
      account: this.account,
      nickname: this.nickname,
      password: this.password,
    };
    const result = await myPost('/register', JSON.stringify(userInfo));
    userInfo = JSON.parse(result);
    // 如果解析完结果之后userInfo.userID有被赋值,说明注册成功了
    if (userInfo.userID) {
      // 显示提示对话框
      promptAction.showToast({
        message: '注册成功!即将返回登录页面...'
      });
      // 2秒后自动返回登录页面
      setTimeout(() => {
        this.pathStack.pop();
      }, 2000);
    } else {
      // 提示服务器返回的错误信息
      promptAction.showDialog({
        message:'error:' + JSON.parse(result).error
      });
    }
  }

  build() {
    NavDestination() {
      Column({ space: 30 }) {
        ...
        Button('注册')
          .type(ButtonType.Normal)
          .borderRadius(5)
          .width('80%')
          .margin(30)
          .onClick(() => {
            this.register();
          })
      }
      .height('100%')
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
    })
  }
}

...

测试下注册账号,效果如下:

我们接着处理登录页面。编写一个成员函数login(),并在点击登录按钮时调用。同时,因为我们需要把返回结果中的昵称和签名同步给首页,所以我们这里用@Provide/@Cosume来共享Index页的nickname和signature变量:

Login.ets 复制代码
import { IUserInfo } from "../models/UserInfoModel"
import { myPost } from "../utils/MyRcp"
import { promptAction } from "@kit.ArkUI"

@Entry
@Component
struct Login {
  @State account: string = ''
  @State password: string = ''
  pathStack: NavPathStack = new NavPathStack()
  @Consume('nickname') nickname: string
  @Consume('signature') signature: string

  async login() {
    // 判断数据合法性
    if (this.account == '' || this.password == '') {
      promptAction.showDialog({
        message: '账号/密码不能为空!'
      });
      return;
    }

    // 数据合法,进行登录操作
    let userInfo: IUserInfo = {
      account: this.account,
      password: this.password,
    }
    let result = await myPost('/login', JSON.stringify(userInfo));
    userInfo = JSON.parse(result);
    // 如果解析完结果之后有token,说明登录成功了
    if (userInfo.token) {
      this.nickname = userInfo.nickname as string;
      this.signature = userInfo.signature as string;
      AppStorage.setOrCreate('token', userInfo.token);
      this.pathStack.pop();
      promptAction.showToast({
        message: '登录成功!即将跳转到首页...'
      });
      setTimeOut(()=>{
        this.pathStack.pop();
      }, 2000);
    } else {
      // 提示服务器返回的错误信息
      promptAction.showDialog({
        message: 'error:' + JSON.parse(result).error
      });
    }
  }

  build() {
    NavDestination() {
      Column({ space: 30 }) {
        ...
        Row() {
          Button('注册').btnExtend()
            .onClick(() => {
              this.pathStack.pushPathByName('register', null);
            })
          Button('登录').btnExtend()
            .onClick(() => {
              this.login();
            })
        }
        .width('100%')
      }
      .height('100%')
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .onReady((content: NavDestinationContext) => {
      this.pathStack = content.pathStack;
    })
  }
}

...

我们测试下登录操作,效果如下:

最后我们来处理首页。首先从首页去登录页的逻辑,我们交给一个切换账号的按钮去承载,然后我们需要增加当从欢迎页自动跳转到首页时的token登录逻辑,最后还需要增加签名设置的逻辑:

Index.ets 复制代码
import { IUserInfo } from '../models/UserInfoModel'
import { myPost } from '../utils/MyRcp'

@Entry
@Component
struct Index {
  @Provide nickname: string = ''
  @Provide signature: string = ''
  @State newSignature: string = ''
  @State editMode: boolean = false
  pathStack:NavPathStack = new NavPathStack()

  async tokenLogin(){
    const result = await myPost('/token_login');
    let userInfo:IUserInfo = JSON.parse(result);
    if (userInfo.userID) {
      this.nickname = userInfo.nickname as string;
      this.signature = userInfo.signature as string;
    }
  }

  async setSignature(){
    let userInfo:IUserInfo = {
      signature:this.newSignature
    }
    const result = await myPost('/set_signature', JSON.stringify(userInfo));
    userInfo = JSON.parse(result);
    if (userInfo.signature != undefined) {
      this.signature = userInfo.signature as string;
    }
  }

  aboutToAppear(): void {
    this.tokenLogin();
  }

  build() {
    Navigation(this.pathStack){
      RelativeContainer() {
        ...
        Button(this.editMode ? '确定修改' : '点击设置签名')
          ...
          .onClick(() => {
            if (this.editMode) {
              this.setSignature();
            }
            this.editMode = !this.editMode;
          })
        ...
        Button('切换账号')
          ...
          .onClick(()=>{
            this.pathStack.pushPathByName('login', null);
          })
      }
      .height('100%')
      .width('100%')
    }
    .mode(NavigationMode.Stack)
  }
}

这样我们首页的逻辑也完成了。测试效果如下:

至此我们整个实战就完成了。当然我们的代码还有很多可以改进的地方,小伙伴们可以自行尝试。

(附:以下是服务器的代码,有能力和兴趣的小伙伴可以下载后自行修改和编译)

main.go 复制代码
package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"sync"

	"github.com/gin-gonic/gin"
)

type User struct {
	Id        int    `json:"id"`
	Account   string `json:"account"`
	Password  string `json:"password"`
	Nickname  string `json:"nickname"`
	Token     string `json:"token"`
	Signature string `json:"signature"`
}

var (
	users     map[string]User
	userMutex sync.Mutex
)

func init() {
	users = make(map[string]User)
	loadUsers()
}

func loadUsers() {
	data, err := os.ReadFile("users.json")
	if err != nil {
		return
	}
	err = json.Unmarshal(data, &users)
	if err != nil {
		log.Println("Error loading users:", err)
	}
}

func saveUsers() {
	data, err := json.Marshal(users)
	if err != nil {
		log.Println("Error saving users:", err)
		return
	}
	err = os.WriteFile("users.json", data, 0644)
	if err != nil {
		log.Println("Error writing users file:", err)
	}
}

func register(c *gin.Context) {
	var newUser User
	if err := c.ShouldBindJSON(&newUser); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
		return
	}

	if newUser.Account == "" || newUser.Password == "" || newUser.Nickname == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
		return
	}

	userMutex.Lock()
	defer userMutex.Unlock()

	if _, exists := users[newUser.Account]; exists {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Account already exists"})
		return
	}

	newUser.Id = len(users) + 1
	users[newUser.Account] = newUser
	saveUsers()

	c.JSON(http.StatusOK, gin.H{
		"userID":   newUser.Id,
		"nickname": newUser.Nickname,
	})
}

func login(c *gin.Context) {
	var loginUser User
	if err := c.ShouldBindJSON(&loginUser); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
		return
	}

	userMutex.Lock()
	defer userMutex.Unlock()

	user, exists := users[loginUser.Account]
	if !exists || user.Password != loginUser.Password {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid account or password"})
		return
	}

	user.Token = generateToken()
	users[user.Account] = user
	saveUsers()

	c.JSON(http.StatusOK, gin.H{
		"userID":    user.Id,
		"nickname":  user.Nickname,
		"signature": user.Signature,
		"token":     user.Token,
	})
}

func tokenLogin(c *gin.Context) {
	token := c.GetHeader("Authorization")
	if token == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is required"})
		return
	}

	userMutex.Lock()
	defer userMutex.Unlock()

	for _, user := range users {
		if user.Token == token {
			c.JSON(http.StatusOK, gin.H{
				"userID":    user.Id,
				"nickname":  user.Nickname,
				"signature": user.Signature,
			})
			return
		}
	}

	c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
}

func setSignature(c *gin.Context) {
	token := c.GetHeader("Authorization")
	if token == "" {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is required"})
		return
	}

	var updateUser User
	if err := c.ShouldBindJSON(&updateUser); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
		return
	}

	userMutex.Lock()
	defer userMutex.Unlock()

	for _, user := range users {
		if user.Token == token {
			user.Signature = updateUser.Signature
			users[user.Account] = user
			saveUsers()
			c.JSON(http.StatusOK, gin.H{"message": "Signature updated successfully", "signature": user.Signature})
			return
		}
	}

	c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
}

func generateToken() string {
	b := make([]byte, 32) // 256 bits are enough for a secure token
	_, err := rand.Read(b)
	if err != nil {
		log.Fatalf("Cannot generate token: %v", err)
	}
	return base64.StdEncoding.EncodeToString(b)
}

func main() {
	// 提示用户输入端口号
	fmt.Println("Please enter the port number(default 8080):")
	var port string
	fmt.Scanln(&port)

	// 尝试将port转换为数字
	portNum, err := strconv.ParseInt(port, 10, 64)
	if err != nil {
		fmt.Println("Invalid port number, default port will be used.")
		portNum = 8080
	}

	port = fmt.Sprintf(":%d", portNum)

	r := gin.Default()

	r.POST("/register", register)
	r.POST("/login", login)
	r.POST("/token_login", tokenLogin)
	r.POST("/set_signature", setSignature)

	r.Run(port)
}
相关推荐
hh.h.8 分钟前
开源鸿蒙生态下Flutter的发展前景分析
flutter·开源·harmonyos
讯方洋哥3 小时前
HarmonyOS应用开发——应用状态
华为·harmonyos
ujainu3 小时前
鸿蒙与Flutter:全场景开发的技术协同与价值
flutter·华为·harmonyos
FrameNotWork4 小时前
HarmonyOS 教学实战:从 0 写一个完整应用(真正能跑、能扩展)
pytorch·华为·harmonyos
Random_index4 小时前
#HarmonyOS篇:鸿蒙开发模板&&三方库axios使用&&跨模块开发交互
harmonyos
游戏技术分享5 小时前
【鸿蒙游戏技术分享 第71期】资质证明文件是否通过
游戏·华为·harmonyos
赵浩生6 小时前
鸿蒙技术干货11:属性动画与转场效果实战
harmonyos
Monkey_247 小时前
鸿蒙开发工具大全
华为·harmonyos
灰灰勇闯IT8 小时前
鸿蒙 5.0 开发入门第二篇:掌握 ArkTS 的 if 分支语句,实现条件逻辑判断
华为·harmonyos
2501_925317139 小时前
[鸿蒙2025领航者闯关] 把小智AI装进「第二大脑」:从开箱到MCP智能体的全链路实战
人工智能·microsoft·harmonyos·鸿蒙2025领航者闯关·小智ai智能音箱·mcp开发