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:
- JSONP (All requests for our project are POST requests)
- Reverse proxy, NGIXN (NGIxN small White)
- Configure the browser (seems not applicable, should, probably, maybe, maybe, maybe)
- 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
-
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!!
- 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
- 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:
- 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
- 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
- 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.
- Create a New SpringBoot project
- 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
- 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
- Package and deploy to the server
- 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
- We can cross domains
Interaction issues with native
Imagine a Web page in three ways:
- Integrated into the app as a native page, this interaction is nothing to say.
- 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
- 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)
-
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**
-
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.