前语: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); // 验证token
      if (!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的时分,只需求经过咱们的多窗口插件翻开一个悬浮窗即可。关于Flutter多窗口的完成能够拜访我之前的文章。

Windows渠道,咱们目前还没有在C++ 服务上运转dart代码,而是经过把窗口设置为0在后台运转着;当需求展现UI的时分,康复窗口巨细,然后进入指定的UI界面即可。

结语

在惯例事务场景根本都不会运用dart开发后台服务;针对整机小型服务的需求,我以为Flutter仍是挺香的,内存不存在隐患,还能前后端都跨渠道。
本篇文章,共享了整个shelf结构编写web服务的经验,我以为在这个小众的类目中这篇文章算是十分齐全了;一起咱们也验证了Flutter/Dart在web服务的可行性,Flutter的事务价值进一步提高~