上一节我们已经把基础的界面搭建和主动跳转给写完了,这一节我们就来把网络请求和处理的逻辑给补全。
首先我们需要在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)
}