Привет, я Андрей, работаю Flutter разработчиком в компании Финам.
После выхода 4й части, где мы подключили мобильное Flutter приложение к сервису Umka, я получил много вопросов от читателей, которые попробовали запустить Web версию приложения и оно в браузере не заработало.
Так будет ли Flutter приложение на базе gRPC сервиса работать в Web?
TLDR: Да, но не получится "стримить" со стороны клиента, а всё остальное будет работать. Для этого нужно сплясать с бубном преобразовать запросы на сервис и ответы с него в формат понятный для браузера. Можно использовать Envoy в качестве Web proxy, который "из коробки" поддерживает входящие/исходящие gRPC запросы.
Ниже я покажу как это сделать. Хочу отметить, что в Гугл идет работа по развитию gRPC для Web и со временем необходимость в "посреднике" может отпасть.
Конфигурация для Envoy proxy
Давайте поместим umka_envoy.yaml файл в проект нашего сервиса. Конфигурация выглядит следующим образом:
static_resources: listeners: - name: umka_listener address: socket_address: { address: 0.0.0.0, port_value: 8888 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager codec_type: auto stat_prefix: ingress_http route_config: name: umka_route virtual_hosts: - name: umka_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: umka_service timeout: 0s max_stream_duration: grpc_timeout_header_max: 0s cors: allow_origin_string_match: - prefix: "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.filters.http.grpc_web - name: envoy.filters.http.cors - name: envoy.filters.http.router clusters: - name: umka_service connect_timeout: 0.25s type: logical_dns http2_protocol_options: {} lb_policy: round_robin load_assignment: cluster_name: cluster_0 endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 0.0.0.0 port_value: 5555
Кратко суть в следующем: клиентским приложением Umka, запущенном в браузере мы будем отправлять запросы и получать ответы на сервер Envoy взаимодействуя с ним через порт 8888. Прокси же в свою очередь будет перенаправлять эти запросы преобразованные в gRPC вызовы на порт 5555, на котором мы и запускали наш сервис в предыдущих четырёх частях. Кластер в нашем примере будет состоять из одного узла (локальный компьютер), на котором и будут работать и Umka sevice и Envoy.
У Envoy хорошая документация и при желании можно познакомиться с этим замечательным продуктом подробнее.
Доработка Flutter приложения
В зависимости от того запущена Web версия Flutter приложения или мобильная, клиентский канал для удаленных gRPC вызовов нужно строить по-разному.
"По дефолту" канал создаётся так:
ClientChannel buildChannel({ required String host, int port = 443, bool secure = true, }) { return ClientChannel(host, port: port, options: ChannelOptions( credentials: secure ? ChannelCredentials.secure() : ChannelCredentials.insecure())); }
В случае запуска в Web так:
ClientChannel buildChannel({ required String host, int port = 443, }) { return GrpcWebClientChannel.xhr(Uri.parse('$host:$port')); }
Я написал небольшую утилитку build_grpc_channel, которая именно этим и "занимается".
Добавим её в зависимости нашего приложения:
dependencies: ... build_grpc_channel: ...
Чуть изменим код класса UmkaService:
const host = 'http://127.0.0.1'; int get port => kIsWeb ? 8888 : 5555; class UmkaService { late final UmkaClient stub; UmkaService() { stub = UmkaClient(buildGrpcChannel(host: host, port: port, secure: false)); } ... }
Канал строим с помощью метода buildGrpcChannel(host: host, port: port, secure: false) из утилиты build_grpc_channel. При запуске в Web передаем порт 8888, на котором запросы будет слушать Envoy.
Вот и все изменения, которые нужно сделать, чтобы приложение заработало в браузере.
Запускаем
На машине должен быть установлен Envoy.
Пример установки на маке: brew install envoy
Проверяем:
envoy --version
Из директории, где расположен конфигурационный файл umka_envoy.yaml выполним команду запуска прокси-сервера:
envoy --config-path umka_envoy.yaml или envoy -c umka_envoy.yaml
Соберём проект для работы в Web и перейдем в его директорию:
flutter build web && cd build/web
Запустим локальный сервер из данной директории build/web, где расположен файл index.html, например php:
php -S localhost:8080
Или с помощью Python:
python -m http.server 8080
Теперь можно открыть в браузере адрес localhost:8080 и проверить работу приложения.
Ложка дёгтя...
Мы видим, что вкладки Quiz и Tutorial работают точно так же, как и мобильной версии приложения, но вот вкладка Exam не работает. Происходит это потому, что код для экзамена подразумевает использование потока данных с "клиента" на сервис
rpc takeExam(stream Answer) returns(Evaluation) {},
а это, на данный момент времени, в gRPC Web не поддерживается.
В направлении от сервиса к "клиенту" стрим работает. Мы видим это по нормальной работе вкладки Tutorial, где используется вызов:
rpc getTutorial(Student) returns (stream AnsweredQuestion) {}
Спасибо всем, кто следил за данной серией статей или прочитал позже. Надеюсь, что было полезно.
