作者:Karl_wei
前言:
Flutter作為跨平臺(tái)的UI框架,其可行性已經(jīng)被市場(chǎng)所認(rèn)可。UI跨端后,我們自然會(huì)希望一些運(yùn)行在終端的小服務(wù)也能跨端
,特別是當(dāng)這個(gè)小服務(wù)還涉及到一些 UI 的展示。
我們希望Flutter能承擔(dān)這個(gè)角色,讓其跨端能力更進(jìn)一步。
需求背景
我們希望在整機(jī)設(shè)備上,運(yùn)行一個(gè)后臺(tái)服務(wù),用戶通過ip地址即可調(diào)用運(yùn)行在設(shè)備上的能力,同時(shí)這個(gè)服務(wù)還能喚起一些UI視圖。
舉個(gè)例子:假如路由器有Android、windows、mac三個(gè)系統(tǒng)的終端,需要提供一個(gè)管理后臺(tái)供用戶設(shè)置,那么路由器的后臺(tái)服務(wù)能力最好是能夠跨這三個(gè)系統(tǒng)的。
web后臺(tái)框架
Dart是支持編寫后臺(tái)服務(wù)的,它提供了 shelf 庫,以處理HTTP請(qǐng)求。整個(gè)項(xiàng)目,我們都是圍繞shelf庫的能力集進(jìn)行開發(fā)的。
靜態(tài)資源 → shelf_static
從需求我們可以了解到,我們需要提供給用戶一個(gè)web管理后臺(tái)進(jìn)行管理,web的資源自然是放在服務(wù)端的。這里我們使用 shelf_static 庫,使用非常的簡(jiǎn)單,就一個(gè)創(chuàng)建靜態(tài)資源操作器的接口。
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_static/shelf_static.dart';
void main() {
var handler = createStaticHandler('example/files',
defaultDocument: 'index.html');
io.serve(handler, 'localhost', 8080);
}
需要注意的是,必須傳入本地的絕對(duì)路徑,指定默認(rèn)的文件入口。Flutter中,資源一般以asset的方式導(dǎo)入,在編譯過程中以二進(jìn)制的形式打包在應(yīng)用中,并不是普通格式的文件,那么如何傳入給createStaticHandler?
我們通過AssetBundle獲取到這些文件的字節(jié)流,并轉(zhuǎn)化成File保存到指定路徑,這個(gè)路徑就是靜態(tài)資源的路徑。
static Future<String> copyAssets() async {
int now = DateTime.now().millisecondsSinceEpoch;
String folderPath = '/sdcard';
final manifestContent = await rootBundle.loadString('AssetManifest.json');
final Map<String, dynamic> manifestMap = json.decode(manifestContent);
final assetList = manifestMap.keys
.where((String key) => key.startsWith('assets/web'))
.toList();
for (final asset in assetList) {
await copyAsset(asset, folderPath);
}
print('移動(dòng)文件耗時(shí) = ${DateTime.now().millisecondsSinceEpoch - now}毫秒');
return '$folderPath/assets/web';
}
static Future<File> copyAsset(String assetName, String localPath) async {
int lastSeparatorIndex = assetName.lastIndexOf('/');
Directory directory = Directory(
'$localPath${Platform.pathSeparator}${assetName.substring(0, lastSeparatorIndex)}');
if (!directory.existsSync()) directory.createSync(recursive: true);
ByteData data = await rootBundle.load(assetName);
Uint8List bytes = data.buffer.asUint8List();
final file = File('$localPath${Platform.pathSeparator}$assetName');
await file.writeAsBytes(bytes);
return file;
}
調(diào)用copyAssets可以拿到路徑,整個(gè)過程一般不會(huì)超過500ms,視文件體積而定。
路由 → shelf_route
現(xiàn)在我們已經(jīng)可以訪問靜態(tài)資源了,接下來需要提供一系列的接口供前端調(diào)用,這個(gè)時(shí)候我們需要用到 shelf_route 庫。shelf_route
支持 RESTful 風(fēng)格的路由,可以處理客戶端的 GET、POST、PUT、DELETE 等 HTTP 請(qǐng)求,也可以從 HTTP 路徑中自動(dòng)提取參數(shù)。每個(gè)路由會(huì)提供request
請(qǐng)求體,最終返回Response
的構(gòu)造函數(shù)即可。
用法很簡(jiǎn)單,下面簡(jiǎn)單演示下如何編寫一個(gè)登錄接口。
import 'package:shelf_router/shelf_router.dart' as self_router;
self_router.Router app = self_router.Router();
// TODO:使用mount,前綴使用模塊命名
app.post(Apis.login, userLogin);
app.post(Apis.resetPwd, resetPassword);
app.post(Apis.signOut, singOutHandle);
Future<Response> userLogin(Request request) async {
final requestBody = await request.readAsString();
final Map<String, dynamic> body = json.decode(requestBody);
Auth auth = Auth();
var info = await auth.getUserInfo();
if (info.$1 == body['username'] && info.$2 == body['password']) {
String token = await auth.generateToken(body['username'], body['password']);
return Response.ok(
BaseResponse(Code.success, data: {'token': token}, msg: '登錄成功')
.toString());
} else {
return Response.ok(BaseResponse(Code.reject, msg: '賬號(hào)密碼錯(cuò)誤').toString());
}
}
中間件 → helf_multipart
一般后臺(tái)服務(wù),都需要對(duì)部分接口進(jìn)行鑒權(quán)操作,這部分的邏輯一般是通用的,一般開發(fā)過程中我們會(huì)用到中間件的機(jī)制
。
中間件通常被用于攔截和處理請(qǐng)求與響應(yīng)之間的過程,以實(shí)現(xiàn)一些公共的應(yīng)用邏輯和功能,比如認(rèn)證、日志記錄、錯(cuò)誤處理等等。
在Flutter中,我們使用 shelf_multipart 這個(gè)庫,通過Pipeline可以加上Middleware,這個(gè)中間件是應(yīng)用于所有路由的,因此某些接口不需要這個(gè)中間件操作,直接在白名單內(nèi)過濾即可;innerHandler則是執(zhí)行對(duì)應(yīng)的響應(yīng)操作。
var middleHandler = const Pipeline().addMiddleware(authMiddleware); // 添加中間件
Middleware authMiddleware = (Handler innerHandler) {
return (Request request) async {
String path = request.url.path.split('?').first;
if (!whitelist.contains(path)) { // 過濾白名單
String? token = request.headers['Authorization'];
Auth auth = Auth();
var authVerify = await auth.verifyToken(token); // 驗(yàn)證token
if (!authVerify.$1) {
return Response.unauthorized(
BaseResponse(Code.reject, msg: authVerify.$2!).toString());
} else {
auth.updateTokenTime(); // 有操作則續(xù)費(fèi)token時(shí)長
}
}
final response = await innerHandler(request);
return response;
};
};
websocket → shelf_websocket
上面所寫的都是提供HTTP服務(wù)的,在業(yè)務(wù)中也經(jīng)常存在需要websocket,我們使用 shelf_websocket 庫。跟靜態(tài)資源一樣,單一的能力只需要提供最簡(jiǎn)單的接口:webSocketHandler
。
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_web_socket/shelf_web_socket.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
void main() {
var webSocketHandler = webSocketHandler((webSocket) {
webSocket.stream.listen((message) {
webSocket.sink.add("echo $message");
});
});
shelf_io.serve(handler, 'localhost', 8080).then((server) {
print('Serving at ws://${server.address.host}:${server.port}');
});
}
最后我們需要把所有的handler都整合成一個(gè)服務(wù),傳給io.serve;
Handler cascadeHandler = Cascade().add(handler).add(app).add(webSocketHandler).handler; // 合并靜態(tài)資源、路由、websocket
// 合入中間件
// 創(chuàng)建本機(jī)服務(wù),端口8888
await io.serve(middleHandler.addHandler(cascadeHandler), '0.0.0.0', 8888);
通用服務(wù)能力
用戶鑒權(quán)
一般這種小型本機(jī)服務(wù),登錄用戶都是互斥的,用戶權(quán)限管理我們可以簡(jiǎn)單的使用:hive + JWT token。
采用hive來保存用戶信息,通過 dart_jsonwebtoken 庫生成token,然后在中間件攔截,對(duì)header中攜帶的token信息進(jìn)行驗(yàn)證,從而達(dá)到鑒權(quán)的目的。
Future<String> generateToken(String userName, String password) async {
Box box = await Hive.openBox(_boxName);
JWT jwt = JWT(
{
'userName': userName,
'password': password,
},
jwtId: const Uuid().v4(),
);
String token = jwt.sign(SecretKey(_secretKey));
await box.put(Constant.userNameKey, userName);
await box.put(Constant.pwdKey, password);
await box.put(Constant.tokenKey, token);
updateTokenTime();
return token;
}
文件上傳
一般web后臺(tái),都會(huì)把文件資源存儲(chǔ)在另一個(gè)文件服務(wù)中,比如:七牛云。不過既然是小服務(wù),我們也希望dart能擁有這個(gè)能力。
文件上傳的路由,參數(shù)一般都是form表單;當(dāng)解析到request為isMultipart時(shí),則對(duì)文件流進(jìn)行讀取,并寫到本地路徑中。
特別需要注意的是:Dart是單線程,寫文件這種耗時(shí)io操作,必須使用IOSink + stream方式寫入,不然內(nèi)存會(huì)拉滿,大文件會(huì)直接讓應(yīng)用崩潰。
app.post(Apis.upload, uploadFile);
Future<Response> uploadFile(Request request) async {
if (!request.isMultipart) {
return Response.ok('Not a multipart request');
} else if (request.isMultipartForm) {
String? filename;
String? path;
await for (var part in request.parts) {
var contentDisposition = part.headers['content-disposition'];
filename = RegExp(r'filename="([^"]*)"')
.firstMatch(contentDisposition!)
?.group(1);
path = '${await CommonUtils.getDownloadPath()}$filename';
File? file = File(path);
IOSink sink = file.openWrite();
await sink.addStream(part);
await sink.flush();
await sink.close();
}
return Response.ok(
BaseResponse(Code.success, data: {"filePath": path}).toString());
}
}
運(yùn)行機(jī)制:Service + UI
使用Flutter編寫這種后臺(tái)服務(wù),還有一個(gè)好處是可以跨平臺(tái)的展示UI。比如:需要后臺(tái)彈出一些設(shè)置成功的toast,這個(gè)時(shí)候就非常的方便了。
Android平臺(tái),我們在Android Service上創(chuàng)建一個(gè)Flutter Engine,可以直接執(zhí)行到Dart代碼;當(dāng)我們需要展示UI的時(shí)候,只需要通過我們的多窗口插件打開一個(gè)懸浮窗即可。
Windows平臺(tái),我們目前還沒有在C++ 服務(wù)上運(yùn)行dart代碼,而是通過把窗口設(shè)置為0在后臺(tái)運(yùn)行著;當(dāng)需要展示UI的時(shí)候,恢復(fù)窗口大小,然后進(jìn)入指定的UI界面即可。
結(jié)語
在常規(guī)業(yè)務(wù)場(chǎng)景基本都不會(huì)使用dart開發(fā)后臺(tái)服務(wù);針對(duì)整機(jī)小型服務(wù)的需求,我認(rèn)為Flutter還是挺香的,內(nèi)存不存在隱患,還能前后端都跨平臺(tái)。
本篇文章,分享了整個(gè)shelf框架編寫web服務(wù)的經(jīng)驗(yàn),我認(rèn)為在這個(gè)小眾的類目中這篇文章算是非常齊全了;同時(shí)我們也驗(yàn)證了Flutter/Dart在web服務(wù)的可行性,F(xiàn)lutter的業(yè)務(wù)價(jià)值進(jìn)一步提升~文章來源:http://www.zghlxwxcb.cn/news/detail-651032.html
Android 學(xué)習(xí)筆錄
Android 性能優(yōu)化篇:https://qr18.cn/FVlo89
Android 車載篇:https://qr18.cn/F05ZCM
Android 逆向安全學(xué)習(xí)筆記:https://qr18.cn/CQ5TcL
Android Framework底層原理篇:https://qr18.cn/AQpN4J
Android 音視頻篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(內(nèi)含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源碼解析筆記:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知識(shí)體:https://qr18.cn/CyxarU
Android 核心筆記:https://qr21.cn/CaZQLo
Android 往年面試題錦:https://qr18.cn/CKV8OZ
2023年最新Android 面試題集:https://qr18.cn/CgxrRy
Android 車載開發(fā)崗位面試習(xí)題:https://qr18.cn/FTlyCJ
音視頻面試題錦:https://qr18.cn/AcV6Ap
文章來源地址http://www.zghlxwxcb.cn/news/detail-651032.html
到了這里,關(guān)于Flutter實(shí)現(xiàn)Service + UI 全面跨平臺(tái)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!