At the beginning of FlutterWeb experience

[toc]

background

Because of recent changes in the needs of the business, in a certain part of the APP page will often change, general situation, said this unstable page should not be borne by the native, modified version of the cost is too big, the most reasonable approach is borne by the H5, and provided by the native necessary bridge to invoke the original method, but due to various historical debt, It didn’t work that way, and after a painful release and waiting for approval, I wondered if flutterWeb could solve this problem.

idea

Page Entry Process

Project architecture Idea

The entire project was converted to support FlutterWeb

The whole project is transformed into FlutterWeb, which can be packaged as Web files and directly deployed on the server. App is still packaged as APK and IPA, but the switch is left at the route listener. When a page needs emergency repair or change, the configuration will be delivered and the WebView or native page will be redirected according to the route configuration.

Pull out a module, a single module supports the Web

Pull out a module referenced by a shell project that packages the module as a Web; At the same time, the module is still referenced by the APP project as a functional module, and only the web products of this module are deployed.

At present, app integrates a certain number of third-party SDK of the native end, which directly supports FlutterWeb engineering with a large amount, so the second method is first tried.

Shell engineering structure drawing

Among them

Flutter_libs is a basic lib library that encapsulates basic network requests, persistent storage, state management, etc. It is also referenced by shell projects and APP projects

Ly_income is a functional module, which is also the module of our main development requirements. It will be referenced by shell project as the packaged content of Web and app project as the original page display.

practice

Packaging problem handling

Since it’s a new project, there aren’t that many hurdles to packaging flutterWeb.

Enabling Web Support

Execute flutter config to view the current configuration information, if any

Settings:
  enable-web: true
  enable-macos-desktop: true
Copy the code

If not, enable the configuration of flutter config –enable-web

Package mode selection

FlutterWeb packaging has two modes to choose from: HTML mode and CanvasKit mode

In particular, they are:

HTML mode

flutter build web –web-renderer html

When we use HTML rendering mode, Flutter uses HTML custom Elements, CSS, Canvas and SVG to render UI elements

Advantage is: the volume is relatively small

Disadvantages: poor rendering performance, cross-end consistency may not be guaranteed

CanvasKit mode

flutter build web –web-renderer canvaskit

When we use Canvaskit rendering mode, Flutter compiles Skia to WebAssembly format and renders using WebGL. Application consistency on mobile and desktop provides better performance and reduces the risk of inconsistent rendering across browsers. But the size of the application will increase by about 2MB.

The advantages are: cross-end consistency is guaranteed, better rendering performance

Disadvantages: Large volume, load page time will be longer

Cross-domain problem handling

I’ve been doing app development, and I’ve heard of cross-domain, but I haven’t seen it yet.

Understand the cross-domain

Cross-domain means that the browser does not execute scripts from other sites, which is a security limitation on JavaScript due to the browser’s same-origin policy

To put it bluntly, when you send a request to another server through the browser, it’s not that the server doesn’t respond, it’s that the server returns a result that is restricted by the browser.

And what is the same origin of the same origin policy

Same-origin indicates that the protocol, domain name, and port must be the same

www.123.com:8080/index.html (HTTP protocol, the www.123.com domain name, port 8080, as long as these three are a different cross-domain, every example here is different)

www.123.com:8080/matsh.html (…

www.123.com:8081/matsh.html (…

Note: Localhost and 127.0.0.1 are cross-domain, although they both point to the local machine.

And the cross-domain solution doesn’t work for me at the moment:

  1. JSONP (All requests for our project are POST requests)
  2. Reverse proxy, NGIXN (NGIxN small White)
  3. Configure the browser (seems not applicable, should, probably, maybe, maybe, maybe)
  4. Cross-domain project configuration (because it is just a trial project and cross-department communication is required if background and operation and maintenance support is needed, which is too troublesome)

From the networkWhat is cross-domainTo delete, invasion

routine
  1. Modify the code when debugging locally to support cross-domain requests

    Add the code — disable-web-Security in the red box above

Then delete the following two files, execute flutter Doctor to generate a new one, and try run again. You will find that the browser already supports cross-domain and you can happily run in the browser interface. But only local debugging is supported!!

  1. Ngixn does forwarding, but this… I haven’t used NGIXN very much, and I need to finish the investigation and give the feasibility report on the weekend, so I don’t have time to study, so I put it aside for now and take a look at it later
  2. The backend and operation and maintenance students help to debug cross-domain, because it is just a trial, there is no need to use the resources of other departments, so it is shelved for now, and if it can be applied in practice later, we will ask for their assistance.
SAO operation

Survival prerequisite:

  1. In fact, this is the way to configure forwarding, but I have no experience in this area, the time is tight and the task is heavy, so I try to do this first
  2. This is an idea similar to OpenFeign, but I don’t know if the backend FeignClient, which is also a bit dangerous, is safer to call the developed interface
  3. Purely personal practice, there must be a better way, but this was the fastest solution I reached at that time, do not spray.

If I can’t ask the background service to do cross-domain, can I ask myself to do cross-domain?

Such as:

I request my server, my server to request the background service, I access the background service across the domain, my server access to the background service is not across the domain, my server across the domain and how, their own things casually pinch.

  1. Create a New SpringBoot project
  2. Set up a controller, the parameters are url full path and parameter JSON string, configure the header after request background service and return information
@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 {
                returnerrMsg().toString(); }}catch (Exception e) {
            returnerrMsg(e.getMessage()).toString(); }}}Copy the code
  1. Configure cross-domain information
@SpringBootConfiguration public class WebGlobalConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); / / set the release which config. The original domain addAllowedOriginPattern (" * "); Config. addAllowedHeader("*"); Config. AddExposedHeader ("*"); Config. addAllowedMethod("GET"); //get config.addAllowedMethod("PUT"); //put config.addAllowedMethod("POST"); //post config.addAllowedMethod("DELETE"); //delete //corsConfig.addAllowedMethod("*"); / / release all request / / whether send cookies config. SetAllowCredentials (true); / / 2. Add the mapping path UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource (); corsConfigurationSource.registerCorsConfiguration("/**", config); CorsFilter return New CorsFilter(corsConfigurationSource); }}Copy the code
  1. Package and deploy to the server
  2. The interface in module no longer requests the background service, but requests my server. Because it is only forwarding, no data structure has been changed, and only the address needs to be changed
  3. We can cross domains

Interaction issues with native

Imagine a Web page in three ways:

  1. Integrated into the app as a native page, this interaction is nothing to say.
  2. Packaged as a Web project and loaded through a WebView, that requires additional handling of fetching and writing persistent information, as well as jumping to and from native pages
  3. There is only URL. The tester can switch the account by transferring parameters in the URL path, which is convenient for testing

For the business, the page load flow should look like this:

Perform different operations in different scenarios
native

Obtain the basic information of the user through the persistence tool class, then read the interface to judge the identity, and make different displays according to the identity. Click the jump time to directly jump through the route

Load via webView

Through JS interaction, get basic user information from the native module (doubtful, whether to directly read the interface? So as to avoid the dependence on native API, if there is a need to modify, you can try not to rely on), and then read the interface to judge the identity, according to the identity of different to do different display, if it is dialog interaction can be directly implemented, if it is the jump page, you can use JS interaction for native operation

Loaded by url

The corresponding user ID can be obtained through the parameter string of URL, and the user information can be obtained by reading the interface. Other operations are described above, but there is no interaction such as page jump

implementation
Get the parameters from the link

For example, the URL is xxx.yyy.zzz/value

How do I get value?

Because the project just happens to use Get for state management, and just happens to be implemented by Get, that’s just how things work in the world. (Looks like Navigator2 already supports this, but haven’t looked closely yet)

  1. Configuring the Routing Table

    
    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; }}Copy the code

    Here appIncome is configured with two route names

    **:fromApp =value**

  2. Get the corresponding value

    Define a bool in the base class and fetch it in the init callback

      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"; }}Copy the code
Operate according to different situations

Take opening in webView as an example. When the page is loaded, the user information is obtained through JS interaction. After receiving the user information, the id and token cached in the cache class are replaced, because the interceptor will read these values for concatenating common parameters

  @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();
    }
  }
Copy the code

It’s disgusting to make this judgment every time, and it should be pulled out and implemented in middleware to avoid coupling this judgment to the page.

This is the normal process of requesting the interface to render the page.

Interaction with native

Here is a reference to his article FlutterWeb’s interaction with Flutter

The only thing to notice is adding a JS to the Web project

There are also a few things to do in the app:

class NativeBridge implements JavascriptChannel { BuildContext context; // from the current widget, easy to use UI Future<WebViewController> _controller; // The controller of the current webView NativeBridge(this.context, this._controller); Get_functions => <String, Function>{"getUserInfo": // getUserInfo => <String, Function>{"getUserInfo": _getUserInfo, "incomeDetail": _incomeDetail, "incomeHistory": _incomeHistory, }; @override String get name => "nativeBridge"; // js through nativebridge.postmessage (MSG); Override get onMessageReceived => (MSG) async {// Convert received string data to JSON Map< string, dynamic> message = json.decode(msg.message); // Asynchronous because some API functions may be implemented asynchronously, such as inputText, waiting for the UI to respond Final data = await _functions[message[" API "]](message["data"]); }; // Get token _getUserInfo(data) async {handlerCallback(data); } // Get 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)")); }}Copy the code

Set channels in the WebView:

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

At the end

So far, it seems that this scheme is feasible. Running an APP page through a web page is really cool, but it is really slow.

Or maybe it’s because my server is a beggar’s version of a beggar and it’s really slow to load:

But it’s fun. The code sucks, but it’s fun.