作者:Karl_wei
前言:
Flutter作為跨平臺的UI框架,其可行性已經被市場所認可。UI跨端后,我們自然會希望一些運行在終端的小服務也能跨端
,特別是當這個小服務還涉及到一些 UI 的展示。
我們希望Flutter能承擔這個角色,讓其跨端能力更進一步。
需求背景
我們希望在整機設備上,運行一個后臺服務,用戶通過ip地址即可調用運行在設備上的能力,同時這個服務還能喚起一些UI視圖。
舉個例子:假如路由器有Android、windows、mac三個系統的終端,需要提供一個管理后臺供用戶設置,那么路由器的后臺服務能力最好是能夠跨這三個系統的。
web后臺框架
Dart是支持編寫后臺服務的,它提供了 shelf 庫,以處理HTTP請求。整個項目,我們都是圍繞shelf庫的能力集進行開發的。
靜態資源 → shelf_static
從需求我們可以了解到,我們需要提供給用戶一個web管理后臺進行管理,web的資源自然是放在服務端的。這里我們使用 shelf_static 庫,使用非常的簡單,就一個創建靜態資源操作器的接口。
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);
}
需要注意的是,必須傳入本地的絕對路徑,指定默認的文件入口。Flutter中,資源一般以asset的方式導入,在編譯過程中以二進制的形式打包在應用中,并不是普通格式的文件,那么如何傳入給createStaticHandler?
我們通過AssetBundle獲取到這些文件的字節流,并轉化成File保存到指定路徑,這個路徑就是靜態資源的路徑。
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('移動文件耗時 = ${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;
}
調用copyAssets可以拿到路徑,整個過程一般不會超過500ms,視文件體積而定。
路由 → shelf_route
現在我們已經可以訪問靜態資源了,接下來需要提供一系列的接口供前端調用,這個時候我們需要用到 shelf_route 庫。
shelf_route
支持 RESTful 風格的路由,可以處理客戶端的 GET、POST、PUT、DELETE 等 HTTP 請求,也可以從 HTTP 路徑中自動提取參數。每個路由會提供request
請求體,最終返回Response
的構造函數即可。
用法很簡單,下面簡單演示下如何編寫一個登錄接口。
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: '賬號密碼錯誤').toString());}
}
中間件 → helf_multipart
一般后臺服務,都需要對部分接口進行鑒權操作,這部分的邏輯一般是通用的,一般開發過程中我們會用到中間件的機制
。
中間件通常被用于攔截和處理請求與響應之間的過程,以實現一些公共的應用邏輯和功能,比如認證、日志記錄、錯誤處理等等。
在Flutter中,我們使用 shelf_multipart 這個庫,通過Pipeline可以加上Middleware,這個中間件是應用于所有路由的,因此某些接口不需要這個中間件操作,直接在白名單內過濾即可;innerHandler則是執行對應的響應操作。
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); // 驗證tokenif (!authVerify.$1) {return Response.unauthorized(BaseResponse(Code.reject, msg: authVerify.$2!).toString());} else {auth.updateTokenTime(); // 有操作則續費token時長}}final response = await innerHandler(request);return response;};
};
websocket → shelf_websocket
上面所寫的都是提供HTTP服務的,在業務中也經常存在需要websocket,我們使用 shelf_websocket 庫。跟靜態資源一樣,單一的能力只需要提供最簡單的接口: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都整合成一個服務,傳給io.serve;
Handler cascadeHandler = Cascade().add(handler).add(app).add(webSocketHandler).handler; // 合并靜態資源、路由、websocket// 合入中間件
// 創建本機服務,端口8888
await io.serve(middleHandler.addHandler(cascadeHandler), '0.0.0.0', 8888);
通用服務能力
用戶鑒權
一般這種小型本機服務,登錄用戶都是互斥的,用戶權限管理我們可以簡單的使用:hive + JWT token。
采用hive來保存用戶信息,通過 dart_jsonwebtoken 庫生成token,然后在中間件攔截,對header中攜帶的token信息進行驗證,從而達到鑒權的目的。
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后臺,都會把文件資源存儲在另一個文件服務中,比如:七牛云。不過既然是小服務,我們也希望dart能擁有這個能力。
文件上傳的路由,參數一般都是form表單;當解析到request為isMultipart時,則對文件流進行讀取,并寫到本地路徑中。
特別需要注意的是:Dart是單線程,寫文件這種耗時io操作,必須使用IOSink + stream方式寫入,不然內存會拉滿,大文件會直接讓應用崩潰。
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());}
}
運行機制:Service + UI
使用Flutter編寫這種后臺服務,還有一個好處是可以跨平臺的展示UI。比如:需要后臺彈出一些設置成功的toast,這個時候就非常的方便了。
Android平臺,我們在Android Service上創建一個Flutter Engine,可以直接執行到Dart代碼;當我們需要展示UI的時候,只需要通過我們的多窗口插件打開一個懸浮窗即可。
Windows平臺,我們目前還沒有在C++ 服務上運行dart代碼,而是通過把窗口設置為0在后臺運行著;當需要展示UI的時候,恢復窗口大小,然后進入指定的UI界面即可。
結語
在常規業務場景基本都不會使用dart開發后臺服務;針對整機小型服務的需求,我認為Flutter還是挺香的,內存不存在隱患,還能前后端都跨平臺。
本篇文章,分享了整個shelf框架編寫web服務的經驗,我認為在這個小眾的類目中這篇文章算是非常齊全了;同時我們也驗證了Flutter/Dart在web服務的可行性,Flutter的業務價值進一步提升~
Android 學習筆錄
Android 性能優化篇:https://qr18.cn/FVlo89
Android 車載篇:https://qr18.cn/F05ZCM
Android 逆向安全學習筆記:https://qr18.cn/CQ5TcL
Android Framework底層原理篇:https://qr18.cn/AQpN4J
Android 音視頻篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(內含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 八大知識體:https://qr18.cn/CyxarU
Android 核心筆記:https://qr21.cn/CaZQLo
Android 往年面試題錦:https://qr18.cn/CKV8OZ
2023年最新Android 面試題集:https://qr18.cn/CgxrRy
Android 車載開發崗位面試習題:https://qr18.cn/FTlyCJ
音視頻面試題錦:https://qr18.cn/AcV6Ap