美文网首页
FlutterWeb初体验

FlutterWeb初体验

作者: 间歇性丶神经病患者 | 来源:发表于2021-12-10 21:12 被阅读0次

    FlutterWeb初体验

    [toc]

    背景

    因为最近业务需求的变动,在APP的某一部分页面会经常性发生变动,一般情况下来说,这种不稳定的页面不应该由原生来承担,修改发版的成本太大了,最合理的做法是由H5来承担,由原生提供必要的bridge来调用原生方法,但是由于种种历史债务,还是没有如此实现,经历了痛苦的发版以及等待审核后,我在想flutterWeb是不是可以解决这个问题?

    想法

    页面进入流程

    st=>start: 开始
    op=>operation: 点击某个页面
    cond=>condition: 通过abTest或者开关服务
    sub1=>subroutine: 进入原生模块
    sub2=>subroutine: 进入Webview页面
    io=>inputoutput: 输入输出框
    e=>end: 进入页面查看信息
    st->op->cond
    cond(yes)->sub2->e
    cond(no)->sub1->e
    
    
    

    项目架构想法

    整个项目转为支持FlutterWeb

    整个项目转为flutterweb,可以打包成web文件直接部署在服务器,而app依旧打包成apk和ipa,但是在路由监听处留下开关,当有页面需要紧急修复或者紧急更改的情况下,下发配置,跳转的时候根据路由配置跳转WebView或者原生页面。

    抽离出某个模块,单个模块支持web

    抽离出一个module,由一个壳工程引用,这个壳工程用于把该module打包成web;同时该模块依然被app工程引用,作为一个功能模块,而部署的时候只部署了这个模块的web产物。

    因为目前app集成了一定数量的原生端的第三方sdk,直接支持flutterweb工程量较大,所以先尝试第二个方法。

    壳工程结构图

    <img src="https://img.haomeiwen.com/i1924616/18f5d8ee85f0f330.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="image.png" style="zoom:50%;" />

    其中

    flutter_libs 是基础的lib库,封装了基础的网络请求,持久化存储,状态管理等基础,壳工程和app工程也会引用

    ly_income是功能module,也是我们主要开发需求的模块,它会被壳工程引用作为web的打包内容,也会被app工程引用作为原生的页面展示。

    实践

    打包问题处理

    因为是新建的项目工程,打包成flutterWeb并不会有那么多障碍。

    开启web支持

    执行 flutter config查看目前的配置信息,如果看到

    Settings:
      enable-web: true
      enable-macos-desktop: true
    

    那就是已经开启了,如果还没,可以使用flutter config --enable-web开启配置

    打包模式选择

    而flutterWeb打包也有两种模式可以选择:html模式和CanvasKit模式

    它们两者各自的特别是:

    html模式

    flutter build web --web-renderer html

    当我们采用html渲染模式时,flutter会采用HTML的custom element,CSS,Canvas和SVG来渲染UI元素

    优点是:体积比较小

    缺点是:渲染性能比较差,跨端一致性可能不受保障

    CanvasKit模式

    flutter build web --web-renderer canvaskit

    当我们采用canvaskit渲染模式时,flutter将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染。应用在移动和桌面端保持一致,有更好的性能,以及降低不同浏览器渲染效果不一致的风险。但是应用的大小会增加大约 2MB。

    优点是:跨端一致性受保障,渲染性能更好

    缺点是:体积比较大,load页面时间会更久

    跨域问题处理

    之前一直是做app开发,跨域这个词只听过,还没见识过。

    了解跨域

    跨域是指浏览器的不执行其他网站脚本的,由于浏览器的同源策略造成,是对JavaScript的一种安全限制

    说白点理解,当你通过浏览器向其他服务器发送请求时,不是服务器不响应,而是服务器返回的结果被浏览器限制了。

    而什么是同源策略的同源

    同源指的是协议、域名、端口 都要保持一致

    http://www.123.com:8080/index.html (http协议,www.123.com 域名、8080 端口 ,只要这三个有一项不一样的都是跨域,这里不一一举例子)

    http://www.123.com:8080/matsh.html(不跨域)

    http://www.123.com:8081/matsh.html(端口不一样,跨域)

    注意:localhost 和127.0.0.1 虽然都指向本机,但也属于跨域。

    而跨域的解决方法也暂时不适用我:

    1. JSONP方式 (我们项目的请求都是post请求)
    2. 反向代理,ngixn (ngixn小白)
    3. 配置浏览器 (好像不太适用,应该,大概,也许,可能,或许)
    4. 项目配置跨域 (因为只是尝试项目,需要后台和运维支持的话,需要跨部门沟通,太麻烦了)

    摘自网络 什么是跨域,侵删歉

    常规做法
    1. 本地调试的时候修改代码,支持跨域请求

      在上图红框中添加代码--disable-web-security

      <img src="https://img.haomeiwen.com/i1924616/e444ef62f7776b1e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="image.png" style="zoom:50%;" />

      <img src="https://img.haomeiwen.com/i1924616/fddf6a72c3a43965.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="image.png" style="zoom:50%;" />

      然后删除以下两个文件,执行flutter doctor生成新的一份,再尝试run起来,你会发现浏览器已经支持跨域了,你可以很开心地在浏览器run接口了。但是仅支持本地调试!!!

      1. ngixn做转发,但是这个... 我没有怎么用过ngixn,而且需要在周末做完调研给出可行性报告,也没有时间去学习,先搁置,后续再拿起来看看
      2. 后端和运维同学帮忙调试跨域,因为是尝试而已,没有必要用到其他部门的资源,先搁置,后续如果可实际应用,再要求他们协助。
    骚操作

    保命前提:

    1. 这个其实就是配置转发的做法,但是这块我没什么经验,时间紧任务重所以就先这么尝试做了
    2. 其实这个就是类似于openfeign之类的想法,但是我并不知道后台开发的FeignClient,而且也有点危险,还是调用开发的接口更加稳妥
    3. 纯个人做法,肯定还会有更好的方法,但是这个是我当时最快的达成方案,勿喷。

    如果说我要求不了后台服务做跨域,那可不可以我自己要求我自己做跨域呢?

    比如:

    我请求我的服务器,我的服务器再去请求后台服务,我访问后台服务跨域而已,我的服务器访问后台服务可不跨域,我的服务器跨域又咋样,自己的东西随便拿捏。

    1. 新建一个springboot项目
    2. 搭建一个controller,参数是url全路径以及参数json字符串,配置好header之后请求后台服务并返回信息
    @CrossOrigin
    @RestController
    @RequestMapping("api/home")
    public class GatewayController {
    
        @PostMapping("/gatewayApi")
        public String gatewayApi(@RequestParam("url") String url, @RequestParam("params") String json) {
            try {
                JSONObject jsonObject = JSONObject.parseObject(json);
                JSONObject result = doPost(jsonObject, url);
                if (result != null) {
                    return result.toString();
                } else {
                    return errMsg().toString();
                }
            } catch (Exception e) {
                return errMsg(e.getMessage()).toString();
            }
        }
    }
    
    1. 配置跨域信息
    @SpringBootConfiguration
    public class WebGlobalConfig {
    
        @Bean
        public CorsFilter corsFilter() {
    
            //创建CorsConfiguration对象后添加配置
            CorsConfiguration config = new CorsConfiguration();
            //设置放行哪些原始域
            config.addAllowedOriginPattern("*");
            //放行哪些原始请求头部信息
            config.addAllowedHeader("*");
            //暴露哪些头部信息
            config.addExposedHeader("*");
            //放行哪些请求方式
            config.addAllowedMethod("GET");     //get
            config.addAllowedMethod("PUT");     //put
            config.addAllowedMethod("POST");    //post
            config.addAllowedMethod("DELETE");  //delete
            //corsConfig.addAllowedMethod("*");     //放行全部请求
    
            //是否发送Cookie
            config.setAllowCredentials(true);
    
            //2. 添加映射路径
            UrlBasedCorsConfigurationSource corsConfigurationSource =
                    new UrlBasedCorsConfigurationSource();
            corsConfigurationSource.registerCorsConfiguration("/**", config);
            //返回CorsFilter
            return new CorsFilter(corsConfigurationSource);
        }
    }
    
    1. 打包后部署到服务器
    2. module里的接口不再请求后台服务,而是请求我的服务器,因为只是转发,所以没有改动任何数据结构,只需要请求地址改动下
    3. 可以跨域了

    与原生交互问题

    设想中web的页面可以有三种方式:

    1. 集成在app里面作为原生页面,这个的交互没什么好说的。
    2. 打包成web项目,通过webview进行加载,那需要额外处理持久化信息的获取与写入,以及与原生页面的跳转交互
    3. 只有url,测试人员可以通过url路径传参之类的切换账号,方便测试

    针对业务来说,页面的加载流程应该是这样的:

    st=>start: 开始
    op=>operation: 进入收益页面
    cond=>operation: 通过接口或者持久化获取用户信息
    role=>condition: 通过接口获取用户身份是否为B角色
    sub1=>subroutine: 展示身份A的页面
    sub2=>subroutine: 展示身份B的页面
    e=>end: 展示对应的信息页面
    st->op->cond->role
    role(yes)->sub2->e
    role(no)->sub1->e
    
    不同场景做不同的操作
    原生

    通过持久化工具类获取用户基础信息,然后读取接口判断身份,根据身份去做不同展示,点击跳转时间也是直接的通过路由跳转

    通过webview加载

    通过js交互,从原生模块拿到用户基础信息(存疑,是否直接读接口?,这样避免对原生api的依赖,如果有需求修改的话可以尽量不依赖),然后读取接口判断身份,根据身份不同去做不同展示,如果是dialog之类的交互可以直接实现,如果是跳转页面之类的,可以通过js交互进行原生操作

    通过url加载的

    通过url的参数串获取到对应的用户id,读取接口获取用户信息,其他操作如上,但是页面没有跳转之类的交互

    实现
    从链接上面获取参数

    比如url为:```http://xxx.yyy.zzz/value

    要如何拿到value值?

    因为项目里刚好使用了Get做状态管理,而刚好Get已经实现了这一块,世间上的事情就是这么刚好。(好像navigator2已经支持这个了,不过还没仔细看过)

    1. 配置路由表
    
    class RouterConf {
      static const String appIncomeArgs = '/app/inCome/:fromApp';
      static const String appIncome = '/app/inCome/';
      static List<GetPage> _getPages = [];
      static List<GetPage> get getPages {
        _getPages = [
          GetPage(name: appIncomeArgs, page: () => const StoreKeeperInComePage()),
        ];
        return _getPages;
      }
    }
    
    

    这里appIncome配置了两个路由名

    但是实际使用时以没带:fromApp为准的,fromApp我觉得可以理解成一个占位符,也就是fromApp=value

    1. 获取对应的value

      在base类里面定义一个bool值,在init的回调里面去做获取操作

        bool ifFromApp = false;
        Map<String, String?> _args = Get.parameters;
        if (_args.isNotEmpty && _args.containsKey('fromApp')) {
            String? _fromAppFlag = Get.parameters['fromApp'];
            if ((_fromAppFlag?.isNotEmpty ?? false)) {
              ifFromApp = _fromAppFlag == "1";
            }
          }
      
    根据不同情景做操作

    以在webview打开为例,在页面加载时通过js交互获取用户信息,拿到用户信息后替换cache类里缓存的id,token之类的,因为拦截器里面会读取这些值用于拼接通用参数

      @override
      void onReady() {
        if (ifFromApp) {
          initUserInfo();
          js.context['getUserInfoCallback'] = getUserInfoCallback;
        }else{
          _loadInterface();
        }
    
        super.onReady();
      }
    
      void initUserInfo() {
        js.context.callMethod("callFlutterMethod", [
          json.encode({
            "api": "getUserInfo",
            "data": {
              "name": 'getUserInfo',
              "needCallback": true,
              "needToken": true,
              "callbackName": 'getUserInfoCallback',
              "callbackArgs": 'info'
            },
          })
        ]);
      }
      
      void getUserInfoCallback(msg, info) {
        Map<String, dynamic> _args = {};
        if (info != null) {
          if (info is String) {
            _args = jsonDecode(info);
          } else {
            _args = info;
          }
          if (_args.containsKey("info")) {
            dynamic _realInfo = _args['info'];
            if (_realInfo is String) {
              _args = jsonDecode(_realInfo);
            } else {
              _args = _realInfo;
            }
          }
          if (_args.containsKey('name')) {
            debugPrint(' _args[name]---------${_args['name']}');
            CacheManager.instance.oName = _args['name'];
          }
          if (_args.containsKey('uId')) {
            debugPrint(' _args[uId]---------${_args['uId']}');
    
            CacheManager.instance.userId = _args['uId'];
          }
          if (_args.containsKey('oId')) {
            debugPrint(' _args[oId]---------${_args['oId']}');
            CacheManager.instance.userOId = _args['oId'];
          }
          if (_args.containsKey('token')) {
            debugPrint(' _args[token]---------${_args['token']}');
    
            CacheManager.instance.userToken = _args['token'];
          }
          if (_args.containsKey('headImg')) {
            debugPrint(' _args[headImg]---------${_args['headImg']}');
            CacheManager.instance.headImgUrl = _args['headImg'];
          }
          state.userName = CacheManager.instance.oName;
          state.userHeaderImg = CacheManager.instance.headImgUrl;
          _loadInterface();
        }
      }
    

    每次都做这个判断是真的恶心,应该把这些东西抽离出来,通过中间件去实现,避免页面上耦合了这个判断。

    接下去就是正常的请求接口渲染页面的流程了。

    与原生的交互

    这里借鉴的是这位大佬的文章 flutterweb与flutter的交互 侵删歉

    唯一需要注意的就是在web项目里面增加一个js

    <img src="https://img.haomeiwen.com/i1924616/af650f09d9300f88.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="image.png" style="zoom:50%;" />

    在app里面也要做一点操作:

    class NativeBridge implements JavascriptChannel {
      BuildContext context; //来源于当前widget, 便于操作UI
      Future<WebViewController> _controller; //当前webView 的 controller
    
      NativeBridge(this.context, this._controller);
    
      // api 与具体函数的映射表,可通过 _functions[key](data) 调用函数
      get _functions => <String, Function>{
            "getUserInfo": _getUserInfo,
            "incomeDetail": _incomeDetail,
            "incomeHistory": _incomeHistory,
          };
    
      @override
      String get name =>
          "nativeBridge"; // js 通过 nativeBridge.postMessage(msg); 调用flutter
    
      // 处理js请求
      @override
      get onMessageReceived => (msg) async {
            // 将收到的string数据转为json
            Map<String, dynamic> message = json.decode(msg.message);
            // 异步是因为有些api函数实现可能为异步,如inputText,等待UI相应
            // 根据 api 字段,调用具体函数
            final data = await _functions[message["api"]](message["data"]);
          };
    
      //拿token
      _getUserInfo(data) async {
        handlerCallback(data);
      } //拿token
    
      _incomeDetail(data) async {
        Get.toNamed(RouterConf.OLD_STOREKEEPER_INCOME_LIST);
      }
    
      _incomeHistory(data) async {
        Get.toNamed(RouterConf.STORE_KEEPER_INCOME_HISTORY);
      }
    
      handlerCallback(data) async {
        LoginModel? _login = await UserManager.getLoginModel();
        UserInfoModel? _user = await UserManager.getUserInfo();
        String? _name = _user?.resultData?.organization?.organizationName;
        String? _uId = _user?.resultData?.user?.userId?.toString() ?? "";
        String? _oId =
            _user?.resultData?.organization?.organizationId?.toString() ?? "";
        String? _token = _login?.resultData?.xAUTHTOKEN;
        String? _img = _user?.resultData?.user?.portraitUrl;
        _img = ImgSize.getImgUrlThumbnail(_img);
        Map<String, dynamic> _infos = {
          "name": _name,
          "uId": _uId,
          "oId": _oId,
          "token": _token,
          "headImg": _img,
        };
    
        if (data['needCallback']) {
          var args = data['callbackArgs'];
          if (data['needToken']) {
            args = "'${data['callbackArgs']}','${jsonEncode(_infos)}'";
          }
          doCallback(data['callbackName'], args);
        }
      }
    
      doCallback(name, args) {
        _controller.then((value) => value.evaluateJavascript("$name($args)"));
      }
    }
    
    

    在webview里面设置channels:

     javascriptChannels: <JavascriptChannel>[
            NativeBridge(context, widget.controller!.future)
          ].toSet(),
    

    结尾

    目前来说好像这个方案是可行的,把一个app页面通过网页跑起来确实是挺爽的,但是慢也是真的慢,

    也可能因为我的服务器是丐版中的丐版,加载起来是真的慢:

    <img src="https://img.haomeiwen.com/i1924616/5860a92dde710996.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="image.png" style="zoom:50%;" />

    <img src="https://img.haomeiwen.com/i1924616/71b2de0857dcbc1e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="image.png" style="zoom:50%;" />

    但是挺好玩的,虽然代码很烂,但是开心就是了。

    相关文章

      网友评论

          本文标题:FlutterWeb初体验

          本文链接:https://www.haomeiwen.com/subject/turhfrtx.html