MAUI加Blazor做一个跨平台的记账APP(五)存储信息和http连接
这篇主要记录通用信息的存储和管理。
比如token的管理。上一篇写了通过登录表单提交后得到token并判断是否登录,有个问题是一旦app退出了,信息没了,打开又是未登录的状态。要是用户每次打开都得登录,那体验是挺不好的。会考虑存储起来,当然如果token过期了,这个逻辑体现在后端,会有相应返回,到时就退出登录状态直到重新请求token。这类似有的app登录后一周都直接用,过了时间后就提示身份失效。
比如后端server地址,总不能每个service文件里定义一个完整的url吧,以后要是后端地址改了,改起来也麻烦。这里打算放在全局配置中。
1. 先说token或者某某配置的版本号之类的信息。
可以用本地文件类似linux常用的pid文件,不过这种显得比较原始;
可以用轻量型的db比如sqlite来存储,但这种又夸张了一点,本来就一个两个信息,不是批量的关系型数据。
不过还有一种键值存储,直接能用,叫做Preference。https://learn.microsoft.com/zh-cn/dotnet/maui/platform-integration/storage/preferences ; 还有用到perference但是更安全的叫做SecureStorage。learn.microsoft.com/zh-cn/dotnet/maui/platform-integration/storage/secure-storage ;
另外还可以参考做前端时会用到的 localStorage。看到一个包叫Jinget Blazor,有GetItemAsync、SetItemAsync之类的方法。没试过这个,但看起来可以用。Jinget Blazor ;
Secure Storage在iOS和Mac上还要额外的配置一下Entitlements.plist,想了一下也没有什么重要的数据需要加密。于是就只采用Preference了。
Preference很简单,直接get和set,把之前的UserService.cs改一下即可。然后app只要没卸载,就能在重新打开后获取上一次的登陆状态。
using System.Text.Json;
using System.Text;
using accountingMAUIBlazor.Models;
namespace accountingMAUIBlazor.Services;
public class UserService : IUserService
{
public bool IsLoggedIn;
public string loginName;
private string _token;
public bool CheckLoggedStatus()
{
IsLoggedIn = Preferences.Get("IsLoggedIn", false); // here
return IsLoggedIn;
}
public string CheckLoginName()
{
loginName = Preferences.Get("loginName", null); // here
return loginName;
}
public async Task<String> GetTokenAsync(string userName, string passWord)
{
_token = Preferences.Get("UserToken", String.Empty); // here
if (!string.IsNullOrWhiteSpace(_token) || IsLoggedIn)
{
Console.WriteLine("no need to request api, return existent token");
return _token;
}
using var httpClient = new HttpClient();
HttpResponseMessage response;
try
{
var loginData = new
{
username = userName,
password = passWord
};
var loginContent = new StringContent(JsonSerializer.Serialize(loginData), Encoding.UTF8, "application/json");
response =
await httpClient.PostAsync("http://127.0.0.1:8000/external/api/login/", loginContent);
response.EnsureSuccessStatusCode();
}
catch (Exception e)
{
Console.WriteLine("Error when request api: " + e.Message);
return _token;
}
var result = await response.Content.ReadAsStringAsync();
Console.WriteLine("result: " + result);
try
{
_token = JsonSerializer.Deserialize<Token>(result)?.token ?? throw new JsonException();
Preferences.Set("UserToken", _token); // here
loginName = JsonSerializer.Deserialize<Token>(result)?.username ?? throw new JsonException();
Preferences.Set("loginName", loginName); // here
IsLoggedIn = true;
Preferences.Set("IsLoggedIn", IsLoggedIn); // here
}
catch (Exception e)
{
Console.WriteLine("Error when handle json: " + e.Message);
return _token;
}
return _token;
}
}
2. 然后是后端server地址,全局http连接等。
原本打算用appsettings.json,但是本地报错了,等后续再来更新这一部分。
至于http连接,总不能每次调用都把url写全吧,这样以后维护更新比较麻烦;而同一个后端接口可能在不同的文件里调用,总不能重复写逻辑吧。于是打算精简这一块。
可以把server地址、header的定义添加token、后端接口列表的定义、基础的操作和处理统一封装一下;
也可以把每一个后端接口的基础操作定义出来。
我目前选择先按前者做一点。
首先除了appsettings.json的使用之外,还可以在MauiProgram.cs里加一句,来配置上http client的base address。
...
builder.Services.AddScoped(hc => new HttpClient { BaseAddress = new Uri("http://127.0.0.1:8000/external/api/") });
...
现在到login.razor里加上inject
@inject HttpClient http
现在到login.razor.cs里随便加一两个语句,打上断点,就能看到http的base address是有值的。
对于service中调用,有一个最简单直接的方式。可以新增一个BackendService.cs文件(比如要是专门请求github的、openai的,可以新增类似的并加上基础信息),定义base address,然后通过传递的值拼接出对应的url,使用比如是" BackendService backend = new(); var url = backend.GetApi("sign_out");"
也可以再定义一个哈希表,存储后端接口的列表,再通过传递接口名来调用,也可以做到在一个地方统一管理,将来修改基本只修改这一个地方。
namespace accountingMAUIBlazor.Services;
public class BackendService
{
public const string baseAddress = "http://127.0.0.1:8000/external/api/";
UriBuilder uriBuilder = new (baseAddress);
public string GetApi(string name)
{
return string.Concat(uriBuilder, name);
}
}
但这种方式也不是完善的,只是能用而已。如果只是少量的请求还好,如果是中大型应用里请求又多的,很容易耗尽连接和端口。
httpclient的使用参考: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests
以及另一篇:The Right Way To Use HttpClient In .NET
我打算再改一下,考虑到可能还要请求其他的后端接口,这里就用上Named Client和IHttpClientFactory。
在MauiProgram.cs中加上一句:
builder.Services.AddHttpClient("Accounting", httpClient => { httpClient.BaseAddress = new Uri("http://127.0.0.1:8000/external/api/"); });
把BackendService改一点,因为考虑到可能会有很多接口,url也许比较长,通过命名的方式来简易获取对应的接口地址,同时也保持在尽量少的地方去维护数据,以免将来有变化时到处改service文件。
namespace accountingMAUIBlazor.Services;
public class BackendService
{
public Dictionary<string, string> apiList = new()
{
{ "login", "login/" },
{ "logout", "logout/" },
...
};
public string GetApiByAlias(string alias)
{
var endpoint = apiList[alias];
return endpoint;
}
}
在UserService.cs中加上IHttpClientFactory,将httpClient的定义换一下,同时请求的url不必写全了:
private readonly IHttpClientFactory _httpClientFactory;
public BackendService backend = new();
public UserService(IHttpClientFactory httpClientFactory) =>
_httpClientFactory = httpClientFactory;
...
//using var httpClient = new HttpClient();
using var httpClient = _httpClientFactory.CreateClient("Accounting");
...
//response = await httpClient.PostAsync("http://127.0.0.1:8000/external/api/login/", loginContent);
//response = await httpClient.PostAsync("login/", loginContent);
response = await httpClient.PostAsync(backend.GetApiByAlias("login"), loginContent);
...
//response = await httpClient.PostAsync("http://127.0.0.1:8000/external/api/logout/", null);
//response = await httpClient.PostAsync("logout/", null);
response = await httpClient.PostAsync(backend.GetApiByAlias("logout"), null);
...